From 322bbc66741434bc611176f6040434d51469c724 Mon Sep 17 00:00:00 2001 From: Tomer Heber Date: Wed, 21 Aug 2024 08:27:31 -0500 Subject: [PATCH 1/4] Feat: fetch Github Installation Id from env0 (#934) * Feat: fetch Github Installation Id from env0 * modify test * modify test * formatting * add example * fix test * fix typo --- .pre-commit-config.yaml | 4 - client/api_client.go | 1 + client/api_client_mock.go | 16 + client/http/client_mock.go | 1 + client/vcs_token_test.go | 46 +++ client/vsc_token.go | 22 ++ env0/data_github_installation_id.go | 46 +++ env0/data_github_installation_id_test.go | 69 ++++ env0/provider.go | 1 + .../data-source.tf | 7 + .../004_template/expected_outputs.json | 22 +- tests/integration/004_template/main.tf | 328 +++++++++--------- 12 files changed, 386 insertions(+), 177 deletions(-) create mode 100644 client/vcs_token_test.go create mode 100644 client/vsc_token.go create mode 100644 env0/data_github_installation_id.go create mode 100644 env0/data_github_installation_id_test.go create mode 100644 examples/data-sources/env0_github_installation_id/data-source.tf diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e1938fed..5bdde3be 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,3 @@ repos: - id: go-vet - id: go-imports - id: go-mod-tidy - - repo: https://github.com/TekWizely/pre-commit-golang - rev: v1.0.0-beta.5 - hooks: - - id: go-staticcheck-mod diff --git a/client/api_client.go b/client/api_client.go index 4c1eefbf..41436a7e 100644 --- a/client/api_client.go +++ b/client/api_client.go @@ -166,6 +166,7 @@ type ApiClientInterface interface { CloudAccountDelete(id string) error CloudAccount(id string) (*CloudAccount, error) CloudAccounts() ([]CloudAccount, error) + VcsToken(vcsType string, repository string) (*VscToken, error) } func NewApiClient(client http.HttpClientInterface, defaultOrganizationId string) ApiClientInterface { diff --git a/client/api_client_mock.go b/client/api_client_mock.go index 6c43f9f4..808f385c 100644 --- a/client/api_client_mock.go +++ b/client/api_client_mock.go @@ -5,6 +5,7 @@ // // mockgen -destination=api_client_mock.go -package=client . ApiClientInterface // + // Package client is a generated GoMock package. package client @@ -2265,6 +2266,21 @@ func (mr *MockApiClientInterfaceMockRecorder) VariablesFromRepository(arg0 any) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VariablesFromRepository", reflect.TypeOf((*MockApiClientInterface)(nil).VariablesFromRepository), arg0) } +// VcsToken mocks base method. +func (m *MockApiClientInterface) VcsToken(arg0, arg1 string) (*VscToken, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "VcsToken", arg0, arg1) + ret0, _ := ret[0].(*VscToken) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// VcsToken indicates an expected call of VcsToken. +func (mr *MockApiClientInterfaceMockRecorder) VcsToken(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VcsToken", reflect.TypeOf((*MockApiClientInterface)(nil).VcsToken), arg0, arg1) +} + // WorkflowTrigger mocks base method. func (m *MockApiClientInterface) WorkflowTrigger(arg0 string) ([]WorkflowTrigger, error) { m.ctrl.T.Helper() diff --git a/client/http/client_mock.go b/client/http/client_mock.go index f0337b3c..151a2b73 100644 --- a/client/http/client_mock.go +++ b/client/http/client_mock.go @@ -5,6 +5,7 @@ // // mockgen -destination=client_mock.go -package=http . HttpClientInterface // + // Package http is a generated GoMock package. package http diff --git a/client/vcs_token_test.go b/client/vcs_token_test.go new file mode 100644 index 00000000..76a1ba3c --- /dev/null +++ b/client/vcs_token_test.go @@ -0,0 +1,46 @@ +package client_test + +import ( + . "github.com/env0/terraform-provider-env0/client" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "go.uber.org/mock/gomock" +) + +var _ = Describe("VCSToken", func() { + vcsType := "github" + repository := "http://myrepo.com/" + + mockVcsToken := VscToken{ + Token: 12345, + } + + Describe("get", func() { + var ret *VscToken + var err error + + BeforeEach(func() { + mockOrganizationIdCall(organizationId) + + httpCall = mockHttpClient.EXPECT(). + Get("/vcs-token/"+vcsType, map[string]string{ + "organizationId": organizationId, + "repository": repository, + }, gomock.Any()). + Do(func(path string, request interface{}, response *VscToken) { + *response = mockVcsToken + }).Times(1) + + ret, err = apiClient.VcsToken(vcsType, repository) + }) + + It("should return vcs token", func() { + Expect(*ret).To(Equal(mockVcsToken)) + }) + + It("should not return error", func() { + Expect(err).To(BeNil()) + }) + }) + +}) diff --git a/client/vsc_token.go b/client/vsc_token.go new file mode 100644 index 00000000..a98557c3 --- /dev/null +++ b/client/vsc_token.go @@ -0,0 +1,22 @@ +package client + +type VscToken struct { + Token int `json:"token"` +} + +func (client *ApiClient) VcsToken(vcsType string, repository string) (*VscToken, error) { + organizationId, err := client.OrganizationId() + if err != nil { + return nil, err + } + + var result VscToken + if err := client.http.Get("/vcs-token/"+vcsType, map[string]string{ + "organizationId": organizationId, + "repository": repository, + }, &result); err != nil { + return nil, err + } + + return &result, nil +} diff --git a/env0/data_github_installation_id.go b/env0/data_github_installation_id.go new file mode 100644 index 00000000..0bc4bfc1 --- /dev/null +++ b/env0/data_github_installation_id.go @@ -0,0 +1,46 @@ +package env0 + +import ( + "context" + + "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 dataGithubInstallationId() *schema.Resource { + return &schema.Resource{ + Description: "returns the github installation id of a git hub repositroy", + + ReadContext: dataGithubInstallationIdRead, + + Schema: map[string]*schema.Schema{ + "repository": { + Type: schema.TypeString, + Description: "the name of the repository", + Required: true, + }, + "github_installation_id": { + Type: schema.TypeInt, + Description: "the github installation id", + Computed: true, + }, + }, + } +} + +func dataGithubInstallationIdRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(client.ApiClientInterface) + + repositroy := d.Get("repository").(string) + + token, err := apiClient.VcsToken("github", repositroy) + if err != nil { + return diag.Errorf("failed to get github installation id: %v", err) + } + + d.Set("github_installation_id", token.Token) + d.SetId(repositroy) + + return nil +} diff --git a/env0/data_github_installation_id_test.go b/env0/data_github_installation_id_test.go new file mode 100644 index 00000000..7b5d319d --- /dev/null +++ b/env0/data_github_installation_id_test.go @@ -0,0 +1,69 @@ +package env0 + +import ( + "errors" + "regexp" + "strconv" + "testing" + + "github.com/env0/terraform-provider-env0/client" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestGithubInstallationIdDataSource(t *testing.T) { + mockToken := client.VscToken{ + Token: 12345, + } + + mockRepositroy := "http://myrepo.com" + + resourceType := "env0_github_installation_id" + resourceName := "test" + accessor := dataSourceAccessor(resourceType, resourceName) + + getValidTestCase := func(repository string) resource.TestCase { + return resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: dataSourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "repository": repository, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "github_installation_id", strconv.Itoa(mockToken.Token)), + ), + }, + }, + } + } + + getErrorTestCase := func(repository string, expectedError string) resource.TestCase { + return resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: dataSourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "repository": repository, + }), + ExpectError: regexp.MustCompile(expectedError), + }, + }, + } + } + + t.Run("get by repository", func(t *testing.T) { + runUnitTest(t, + getValidTestCase(mockRepositroy), + func(mock *client.MockApiClientInterface) { + mock.EXPECT().VcsToken("github", mockRepositroy).Return(&mockToken, nil).AnyTimes() + }, + ) + }) + + t.Run("get by repository - failed", func(t *testing.T) { + runUnitTest(t, + getErrorTestCase(mockRepositroy, "failed to get github installation id: error"), + func(mock *client.MockApiClientInterface) { + mock.EXPECT().VcsToken("github", mockRepositroy).Return(nil, errors.New("error")).AnyTimes() + }, + ) + }) +} diff --git a/env0/provider.go b/env0/provider.go index 7568a0cd..1c2d29a4 100644 --- a/env0/provider.go +++ b/env0/provider.go @@ -103,6 +103,7 @@ func Provider(version string) plugin.ProviderFunc { "env0_projects": dataProjects(), "env0_module_testing_project": dataModuleTestingProject(), "env0_variable_set": dataVariableSet(), + "env0_github_installation_id": dataGithubInstallationId(), }, ResourcesMap: map[string]*schema.Resource{ "env0_project": resourceProject(), diff --git a/examples/data-sources/env0_github_installation_id/data-source.tf b/examples/data-sources/env0_github_installation_id/data-source.tf new file mode 100644 index 00000000..ea528cf4 --- /dev/null +++ b/examples/data-sources/env0_github_installation_id/data-source.tf @@ -0,0 +1,7 @@ +data "env0_github_installation_id" "example" { + repository = "https://github.com/env0/templates" +} + +output "github_installation_id" { + value = data.env0_github_installation_id.example.github_installation_id +} diff --git a/tests/integration/004_template/expected_outputs.json b/tests/integration/004_template/expected_outputs.json index eb83744a..72a43051 100644 --- a/tests/integration/004_template/expected_outputs.json +++ b/tests/integration/004_template/expected_outputs.json @@ -1,11 +1,11 @@ -{ - "github_template_type": "terraform", - "github_template_name": "Github Test-", - "github_template_repository": "https://github.com/env0/templates", - "gitlab_template_repository": "https://gitlab.com/env0/gitlab-vcs-integration-tests.git", - "github_template_path": "/second", - "tg_tg_version" : "0.35.0", - "data_github_template_type": "terraform", - "github_variables_name": "email", - "github_variables_value": "default@domain.com" -} +{ + "github_template_type": "terraform", + "github_template_name": "Github Test-", + "github_template_repository": "https://github.com/env0/templates", + "gitlab_template_repository": "https://gitlab.com/env0/gitlab-vcs-integration-tests.git", + "github_template_path": "/second", + "tg_tg_version": "0.35.0", + "data_github_template_type": "terraform", + "github_variables_name": "email", + "github_variables_value": "default@domain.com" +} \ No newline at end of file diff --git a/tests/integration/004_template/main.tf b/tests/integration/004_template/main.tf index e12de624..ae32840e 100644 --- a/tests/integration/004_template/main.tf +++ b/tests/integration/004_template/main.tf @@ -1,162 +1,166 @@ -provider "random" {} - -resource "random_string" "random" { - length = 8 - special = false - min_lower = 8 -} - -# Github Integration must be done manually - so we expect an existing Github Template with this name - -# It must be for https://github.com/env0/templates - We validate that in the outputs -data "env0_template" "github_template" { - name = "Github Integrated Template" -} - -# Gitlab Integration must be done manually - so we expect an existing Gitlab Template with this name -# It must be for https://gitlab.com/env0/gitlab-vcs-integration-tests - the gitlab_project_id is still static -data "env0_template" "gitlab_template" { - name = "Gitlab Integrated Template" -} - -resource "env0_template" "github_template" { - name = "Github Test-${random_string.random.result}" - description = "Template description - GitHub" - type = "terraform" - 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" - retries_on_deploy = 3 - retry_on_deploy_only_when_matches_regex = "abc" - retries_on_destroy = 1 - terraform_version = "0.15.1" -} - -resource "env0_template" "gitlab_template" { - name = "GitLab Test-${random_string.random.result}" - description = "Template description - Gitlab" - type = "terraform" - repository = data.env0_template.gitlab_template.repository - token_id = data.env0_template.gitlab_template.token_id - gitlab_project_id = 32315446 - path = var.second_run ? "second" : "misc/null-resource" - retries_on_deploy = 3 - retry_on_deploy_only_when_matches_regex = "abc" - retries_on_destroy = 1 - terraform_version = "0.15.1" -} - -resource "env0_template" "template_tg" { - name = "Template for environment resource - tg-${random_string.random.result}" - type = "terragrunt" - repository = "https://github.com/env0/templates" - path = "terragrunt/misc/null-resource" - terraform_version = "0.15.1" - terragrunt_version = "0.35.0" -} - -resource "env0_template" "template_opentofu" { - name = "Opentofu-${random_string.random.result}" - description = "Template description - OpenTofu and GitHub" - type = "opentofu" - repository = data.env0_template.github_template.repository - github_installation_id = data.env0_template.github_template.github_installation_id - path = "/misc/null-resource" - retries_on_deploy = 3 - retry_on_deploy_only_when_matches_regex = "abc" - retries_on_destroy = 1 - opentofu_version = var.second_run ? "1.6.0" : "RESOLVE_FROM_CODE" -} - -resource "env0_template" "template_ansible" { - name = "Ansible-${random_string.random.result}" - description = "Template description - Ansible and GitHub" - type = "ansible" - repository = data.env0_template.github_template.repository - github_installation_id = data.env0_template.github_template.github_installation_id - path = "/misc/null-resource" - retries_on_deploy = 3 - retry_on_deploy_only_when_matches_regex = "abc" - retries_on_destroy = 1 - ansible_version = var.second_run ? "3.6.0" : "latest" -} - -resource "env0_configuration_variable" "in_a_template" { - name = "fake_key" - value = "fake value" - template_id = env0_template.github_template.id -} - -resource "env0_configuration_variable" "in_a_template2" { - name = "fake_key_2" - value = "fake value 2" - template_id = env0_template.github_template.id - type = "terraform" -} - -resource "env0_template" "github_template_source_code" { - name = "Github Test Source Code-${random_string.random.result}" - description = "Template description - GitHub" - type = "terraform" - repository = data.env0_template.github_template.repository - github_installation_id = data.env0_template.github_template.github_installation_id - path = "misc/custom-flow-tf-vars" - retries_on_deploy = 3 - retry_on_deploy_only_when_matches_regex = "abc" - retries_on_destroy = 1 - terraform_version = "0.15.1" -} - -resource "env0_template" "helm_template" { - name = "helm-${random_string.random.result}-1" - description = "Template description helm" - repository = "https://github.com/env0/templates" - path = "misc/helm/dummy" - type = "helm" -} - -resource "env0_template" "helm_template_repo" { - name = "helm-${random_string.random.result}-2" - description = "Template description helm repo" - repository = "https://charts.bitnami.com/bitnami" - type = "helm" - helm_chart_name = "nginx" - is_helm_repository = true -} - -data "env0_source_code_variables" "variables" { - template_id = env0_template.github_template_source_code.id -} - -output "github_variables_name" { - value = data.env0_source_code_variables.variables.variables.0.name -} - -output "github_variables_value" { - value = data.env0_source_code_variables.variables.variables.0.value -} - -output "github_template_id" { - value = env0_template.github_template.id -} -output "github_template_type" { - value = env0_template.github_template.type -} -output "github_template_name" { - value = replace(env0_template.github_template.name, random_string.random.result, "") -} -output "github_template_repository" { - value = env0_template.github_template.repository -} -output "gitlab_template_repository" { - value = env0_template.gitlab_template.repository -} -output "github_template_path" { - value = env0_template.github_template.path -} -output "tg_tg_version" { - value = env0_template.template_tg.terragrunt_version -} - -output "data_github_template_type" { - value = data.env0_template.github_template.type -} +provider "random" {} + +resource "random_string" "random" { + length = 8 + special = false + min_lower = 8 +} + +# Github Integration must be done manually - so we expect an existing Github Template with this name - +# It must be for https://github.com/env0/templates - We validate that in the outputs +data "env0_template" "github_template" { + name = "Github Integrated Template" +} + +# Gitlab Integration must be done manually - so we expect an existing Gitlab Template with this name +# It must be for https://gitlab.com/env0/gitlab-vcs-integration-tests - the gitlab_project_id is still static +data "env0_template" "gitlab_template" { + name = "Gitlab Integrated Template" +} + +resource "env0_template" "github_template" { + name = "Github Test-${random_string.random.result}" + description = "Template description - GitHub" + type = "terraform" + 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" + retries_on_deploy = 3 + retry_on_deploy_only_when_matches_regex = "abc" + retries_on_destroy = 1 + terraform_version = "0.15.1" +} + +data "env0_github_installation_id" "github_installation_id" { + repository = data.env0_template.github_template.repository +} + +resource "env0_template" "gitlab_template" { + name = "GitLab Test-${random_string.random.result}" + description = "Template description - Gitlab" + type = "terraform" + repository = data.env0_template.gitlab_template.repository + token_id = data.env0_template.gitlab_template.token_id + gitlab_project_id = 32315446 + path = var.second_run ? "second" : "misc/null-resource" + retries_on_deploy = 3 + retry_on_deploy_only_when_matches_regex = "abc" + retries_on_destroy = 1 + terraform_version = "0.15.1" +} + +resource "env0_template" "template_tg" { + name = "Template for environment resource - tg-${random_string.random.result}" + type = "terragrunt" + repository = "https://github.com/env0/templates" + path = "terragrunt/misc/null-resource" + terraform_version = "0.15.1" + terragrunt_version = "0.35.0" +} + +resource "env0_template" "template_opentofu" { + name = "Opentofu-${random_string.random.result}" + description = "Template description - OpenTofu and GitHub" + type = "opentofu" + repository = data.env0_template.github_template.repository + github_installation_id = data.env0_template.github_template.github_installation_id + path = "/misc/null-resource" + retries_on_deploy = 3 + retry_on_deploy_only_when_matches_regex = "abc" + retries_on_destroy = 1 + opentofu_version = var.second_run ? "1.6.0" : "RESOLVE_FROM_CODE" +} + +resource "env0_template" "template_ansible" { + name = "Ansible-${random_string.random.result}" + description = "Template description - Ansible and GitHub" + type = "ansible" + repository = data.env0_template.github_template.repository + github_installation_id = data.env0_template.github_template.github_installation_id + path = "/misc/null-resource" + retries_on_deploy = 3 + retry_on_deploy_only_when_matches_regex = "abc" + retries_on_destroy = 1 + ansible_version = var.second_run ? "3.6.0" : "latest" +} + +resource "env0_configuration_variable" "in_a_template" { + name = "fake_key" + value = "fake value" + template_id = env0_template.github_template.id +} + +resource "env0_configuration_variable" "in_a_template2" { + name = "fake_key_2" + value = "fake value 2" + template_id = env0_template.github_template.id + type = "terraform" +} + +resource "env0_template" "github_template_source_code" { + name = "Github Test Source Code-${random_string.random.result}" + description = "Template description - GitHub" + type = "terraform" + repository = data.env0_template.github_template.repository + github_installation_id = data.env0_template.github_template.github_installation_id + path = "misc/custom-flow-tf-vars" + retries_on_deploy = 3 + retry_on_deploy_only_when_matches_regex = "abc" + retries_on_destroy = 1 + terraform_version = "0.15.1" +} + +resource "env0_template" "helm_template" { + name = "helm-${random_string.random.result}-1" + description = "Template description helm" + repository = "https://github.com/env0/templates" + path = "misc/helm/dummy" + type = "helm" +} + +resource "env0_template" "helm_template_repo" { + name = "helm-${random_string.random.result}-2" + description = "Template description helm repo" + repository = "https://charts.bitnami.com/bitnami" + type = "helm" + helm_chart_name = "nginx" + is_helm_repository = true +} + +data "env0_source_code_variables" "variables" { + template_id = env0_template.github_template_source_code.id +} + +output "github_variables_name" { + value = data.env0_source_code_variables.variables.variables.0.name +} + +output "github_variables_value" { + value = data.env0_source_code_variables.variables.variables.0.value +} + +output "github_template_id" { + value = env0_template.github_template.id +} +output "github_template_type" { + value = env0_template.github_template.type +} +output "github_template_name" { + value = replace(env0_template.github_template.name, random_string.random.result, "") +} +output "github_template_repository" { + value = env0_template.github_template.repository +} +output "gitlab_template_repository" { + value = env0_template.gitlab_template.repository +} +output "github_template_path" { + value = env0_template.github_template.path +} +output "tg_tg_version" { + value = env0_template.template_tg.terragrunt_version +} + +output "data_github_template_type" { + value = data.env0_template.github_template.type +} From 3c77f30349f58c4619367dd6f8303f5cb60b87b7 Mon Sep 17 00:00:00 2001 From: update generated docs action Date: Wed, 21 Aug 2024 13:28:21 +0000 Subject: [PATCH 2/4] Update docs --- docs/data-sources/github_installation_id.md | 35 +++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 docs/data-sources/github_installation_id.md diff --git a/docs/data-sources/github_installation_id.md b/docs/data-sources/github_installation_id.md new file mode 100644 index 00000000..3f95eb06 --- /dev/null +++ b/docs/data-sources/github_installation_id.md @@ -0,0 +1,35 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "env0_github_installation_id Data Source - terraform-provider-env0" +subcategory: "" +description: |- + returns the github installation id of a git hub repositroy +--- + +# env0_github_installation_id (Data Source) + +returns the github installation id of a git hub repositroy + +## Example Usage + +```terraform +data "env0_github_installation_id" "example" { + repository = "https://github.com/env0/templates" +} + +output "github_installation_id" { + value = data.env0_github_installation_id.example.github_installation_id +} +``` + + +## Schema + +### Required + +- `repository` (String) the name of the repository + +### Read-Only + +- `github_installation_id` (Number) the github installation id +- `id` (String) The ID of this resource. From cff9b13e8e47cac82651df55bdef0e5b4f45940d Mon Sep 17 00:00:00 2001 From: Tomer Heber Date: Wed, 21 Aug 2024 09:43:44 -0500 Subject: [PATCH 3/4] Feat: allow is_remote_apply on env0_environment creation (#936) --- .golangci.yaml | 2 ++ env0/errors.go | 2 ++ env0/resource_environment.go | 13 +++++-------- env0/resource_environment_test.go | 17 +---------------- 4 files changed, 10 insertions(+), 24 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 25cada2c..c2815a27 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -29,3 +29,5 @@ linters-settings: errcheck: exclude-functions: - (*github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema.ResourceData).Set + goconst: + ignore-tests: true diff --git a/env0/errors.go b/env0/errors.go index 07bea238..2f7c28b2 100644 --- a/env0/errors.go +++ b/env0/errors.go @@ -11,6 +11,8 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) +var ErrNoChanges = errors.New("no changes") + func driftDetected(err error) bool { var failedResponseError *http.FailedResponseError if errors.As(err, &failedResponseError) && failedResponseError.NotFound() { diff --git a/env0/resource_environment.go b/env0/resource_environment.go index 0c3cf60b..ea7e6e77 100644 --- a/env0/resource_environment.go +++ b/env0/resource_environment.go @@ -601,11 +601,6 @@ func validateTemplateProjectAssignment(d *schema.ResourceData, apiClient client. func resourceEnvironmentCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { apiClient := meta.(client.ApiClientInterface) - isRemoteApplyEnabled := d.Get("is_remote_apply_enabled").(bool) - if isRemoteApplyEnabled { - return diag.Errorf("is_remote_apply_enabled cannot be set when creating a new environment. Set this value after the environment is created") - } - environmentPayload, createEnvPayloadErr := getCreatePayload(d, apiClient) if createEnvPayloadErr != nil { return createEnvPayloadErr @@ -867,6 +862,7 @@ func deploy(d *schema.ResourceData, apiClient client.ApiClientInterface) diag.Di if err != nil { return diag.Errorf("failed deploying environment: %v", err) } + d.Set("deployment_id", deployResponse.Id) return nil @@ -1148,7 +1144,7 @@ func getEnvironmentConfigurationSetChanges(d *schema.ResourceData, apiClient cli } if assignVariableSets == nil && unassignVariableSets == nil { - return nil, nil + return nil, ErrNoChanges } return &client.ConfigurationSetChanges{ @@ -1192,7 +1188,7 @@ func getDeployPayload(d *schema.ResourceData, apiClient client.ApiClientInterfac } payload.ConfigurationSetChanges, err = getEnvironmentConfigurationSetChanges(d, apiClient) - if err != nil { + if err != nil && !errors.Is(err, ErrNoChanges) { return client.DeployRequest{}, err } } @@ -1341,7 +1337,7 @@ func getEnvironmentByName(meta interface{}, name string, projectId string, exclu return client.Environment{}, diag.Errorf("Could not get Environment: %v", err) } - var filteredEnvironments []client.Environment + filteredEnvironments := []client.Environment{} for _, candidate := range environmentsByName { if excludeArchived && candidate.IsArchived != nil && *candidate.IsArchived { continue @@ -1394,6 +1390,7 @@ func resourceEnvironmentImport(ctx context.Context, d *schema.ResourceData, meta } apiClient := meta.(client.ApiClientInterface) + d.SetId(environment.Id) scope := client.ScopeEnvironment diff --git a/env0/resource_environment_test.go b/env0/resource_environment_test.go index 8506d8ce..5ad62f1f 100644 --- a/env0/resource_environment_test.go +++ b/env0/resource_environment_test.go @@ -997,6 +997,7 @@ func TestUnitEnvironmentResource(t *testing.T) { format := "" for _, variable := range variables { schemaFormat := "" + if variable.Schema != nil { if variable.Schema.Enum != nil { schemaFormat = fmt.Sprintf(` @@ -1978,22 +1979,6 @@ func TestUnitEnvironmentResource(t *testing.T) { } testValidationFailures := func() { - t.Run("create environment with is_remote_apply_enabled set to 'true'", func(t *testing.T) { - runUnitTest(t, resource.TestCase{ - Steps: []resource.TestStep{ - { - Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ - "name": environment.Name, - "project_id": environment.ProjectId, - "template_id": environment.LatestDeploymentLog.BlueprintId, - "is_remote_apply_enabled": true, - }), - ExpectError: regexp.MustCompile("is_remote_apply_enabled cannot be set when creating a new environment"), - }, - }, - }, func(mockFunc *client.MockApiClientInterface) {}) - }) - t.Run("Failure in validation while glob is enabled and pathChanges no", func(t *testing.T) { autoDeployWithCustomGlobEnabled := resource.TestCase{ Steps: []resource.TestStep{ From 4de999dcd6da22e8aa0921bc70a364e1da3dad73 Mon Sep 17 00:00:00 2001 From: Tomer Heber Date: Wed, 21 Aug 2024 11:51:12 -0500 Subject: [PATCH 4/4] Fix: drift in value of env0_configuration_variable in every plan (#937) * Fix: drift in value of env0_configuration_variable in every plan * fix invalid memory error * fix data_source_code_variables * Fix unit test * fix test --- client/configuration_variable.go | 2 +- env0/data_configuration_variable.go | 4 ++ env0/data_source_code_variables.go | 12 ++++- env0/resource_configuration_variable.go | 12 +++-- env0/resource_configuration_variable_test.go | 47 ++++++++++++++++++-- env0/utils_test.go | 6 --- tests/harness.go | 2 +- 7 files changed, 70 insertions(+), 15 deletions(-) diff --git a/client/configuration_variable.go b/client/configuration_variable.go index 77d6e462..67074798 100644 --- a/client/configuration_variable.go +++ b/client/configuration_variable.go @@ -41,7 +41,7 @@ func (c *ConfigurationVariableSchema) ResourceDataSliceStructValueWrite(values m type ConfigurationVariable struct { ScopeId string `json:"scopeId,omitempty"` - Value string `json:"value"` + Value string `json:"value" tfschema:"-"` OrganizationId string `json:"organizationId,omitempty"` UserId string `json:"userId,omitempty"` IsSensitive *bool `json:"isSensitive,omitempty"` diff --git a/env0/data_configuration_variable.go b/env0/data_configuration_variable.go index b3383104..d1f7d0ad 100644 --- a/env0/data_configuration_variable.go +++ b/env0/data_configuration_variable.go @@ -136,6 +136,10 @@ func dataConfigurationVariableRead(ctx context.Context, d *schema.ResourceData, return diag.Errorf("schema resource data serialization failed: %v", err) } + if variable.IsSensitive == nil || !*variable.IsSensitive { + d.Set("value", variable.Value) + } + d.Set("enum", variable.Schema.Enum) if variable.Schema.Format != client.Text { diff --git a/env0/data_source_code_variables.go b/env0/data_source_code_variables.go index f180b7b3..c265302b 100644 --- a/env0/data_source_code_variables.go +++ b/env0/data_source_code_variables.go @@ -76,10 +76,20 @@ func dataSourceCodeVariablesRead(ctx context.Context, d *schema.ResourceData, me return diag.Errorf("failed to extract variables from repository: %v", err) } - if err := writeResourceDataSlice(variables, "variables", d); err != nil { + ivalues, err := writeResourceDataGetSliceValues(variables, "variables", d) + if err != nil { return diag.Errorf("schema slice resource data serialization failed: %v", err) } + for i, ivalue := range ivalues { + if variables[i].IsSensitive == nil || !*variables[i].IsSensitive { + ivariable := ivalue.(map[string]interface{}) + ivariable["value"] = variables[i].Value + } + } + + d.Set("variables", ivalues) + d.SetId(templateId) return nil diff --git a/env0/resource_configuration_variable.go b/env0/resource_configuration_variable.go index f3c05b67..6122c9f1 100644 --- a/env0/resource_configuration_variable.go +++ b/env0/resource_configuration_variable.go @@ -170,7 +170,7 @@ func getConfigurationVariableCreateParams(d *schema.ResourceData) (*client.Confi func resourceConfigurationVariableCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { params, err := getConfigurationVariableCreateParams(d) if err != nil { - return diag.Errorf(err.Error()) + return diag.FromErr(err) } apiClient := meta.(client.ApiClientInterface) @@ -195,7 +195,9 @@ func getEnum(d *schema.ResourceData, selectedValue string) ([]string, error) { if enumValue == nil { return nil, fmt.Errorf("an empty enum value is not allowed (at index %d)", i) } + actualEnumValues = append(actualEnumValues, enumValue.(string)) + if enumValue == selectedValue { valueExists = true } @@ -223,13 +225,17 @@ func resourceConfigurationVariableRead(ctx context.Context, d *schema.ResourceDa return diag.Errorf("schema resource data serialization failed: %v", err) } + if variable.IsSensitive == nil || !*variable.IsSensitive { + d.Set("value", variable.Value) + } + return nil } func resourceConfigurationVariableUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { params, err := getConfigurationVariableCreateParams(d) if err != nil { - return diag.Errorf(err.Error()) + return diag.FromErr(err) } apiClient := meta.(client.ApiClientInterface) @@ -280,7 +286,7 @@ func resourceConfigurationVariableImport(ctx context.Context, d *schema.Resource var scopeName string if variable.Scope == client.ScopeTemplate { - scopeName = strings.ToLower(fmt.Sprintf("%s_id", templateScope)) + scopeName = strings.ToLower(templateScope + "_id") } else { scopeName = strings.ToLower(fmt.Sprintf("%s_id", variable.Scope)) } diff --git a/env0/resource_configuration_variable_test.go b/env0/resource_configuration_variable_test.go index 26613e2e..063d7512 100644 --- a/env0/resource_configuration_variable_test.go +++ b/env0/resource_configuration_variable_test.go @@ -11,6 +11,7 @@ import ( "github.com/env0/terraform-provider-env0/client" "github.com/env0/terraform-provider-env0/client/http" + "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "go.uber.org/mock/gomock" ) @@ -50,6 +51,7 @@ func TestUnitConfigurationVariableResource(t *testing.T) { IsRequired: *configVar.IsRequired, IsReadOnly: *configVar.IsReadOnly, } + t.Run("Create", func(t *testing.T) { createTestCase := resource.TestCase{ Steps: []resource.TestStep{ @@ -74,6 +76,47 @@ func TestUnitConfigurationVariableResource(t *testing.T) { }) }) + t.Run("Create sensitive", func(t *testing.T) { + createSenstiveConfig := client.ConfigurationVariableCreateParams{ + Name: "name", + Value: "value", + IsSensitive: true, + Scope: client.ScopeGlobal, + } + + sensitiveConfig := client.ConfigurationVariable{ + Id: uuid.NewString(), + Name: createSenstiveConfig.Name, + Value: "*", + IsSensitive: boolPtr(true), + Scope: client.ScopeGlobal, + } + + createTestCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": createSenstiveConfig.Name, + "value": createSenstiveConfig.Value, + "is_sensitive": true, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", sensitiveConfig.Id), + resource.TestCheckResourceAttr(accessor, "name", createSenstiveConfig.Name), + resource.TestCheckResourceAttr(accessor, "value", createSenstiveConfig.Value), + resource.TestCheckResourceAttr(accessor, "is_sensitive", strconv.FormatBool(true)), + ), + }, + }, + } + + runUnitTest(t, createTestCase, func(mock *client.MockApiClientInterface) { + mock.EXPECT().ConfigurationVariableCreate(createSenstiveConfig).Times(1).Return(sensitiveConfig, nil) + mock.EXPECT().ConfigurationVariablesById(sensitiveConfig.Id).Times(1).Return(sensitiveConfig, nil) + mock.EXPECT().ConfigurationVariableDelete(sensitiveConfig.Id).Times(1).Return(nil) + }) + }) + t.Run("Create Two with readonly", func(t *testing.T) { // https://github.com/env0/terraform-provider-env0/issues/215 // we want to create two variables, one org level with read only and another in lower level and see we can still manage both - double apply and destroy @@ -286,7 +329,6 @@ resource "{{.resourceType}}" "{{.projResourceName}}" { for _, format := range []client.Format{client.HCL, client.JSON} { t.Run("Create "+string(format)+" Variable", func(t *testing.T) { - expectedVariable := `{ A = "A" B = "B" @@ -627,8 +669,8 @@ resource "%s" "test" { IsRequired: *configVarImport.IsRequired, IsReadOnly: *configVarImport.IsReadOnly, } - t.Run("import by name", func(t *testing.T) { + t.Run("import by name", func(t *testing.T) { createTestCaseForImport := resource.TestCase{ Steps: []resource.TestStep{ { @@ -653,7 +695,6 @@ resource "%s" "test" { }) t.Run("import by id", func(t *testing.T) { - createTestCaseForImport := resource.TestCase{ Steps: []resource.TestStep{ { diff --git a/env0/utils_test.go b/env0/utils_test.go index ab8f3967..21f93d05 100644 --- a/env0/utils_test.go +++ b/env0/utils_test.go @@ -177,7 +177,6 @@ func TestWriteCustomResourceData(t *testing.T) { Name: "name0", Description: "desc0", ScopeId: "scope0", - Value: "value0", OrganizationId: "organization0", UserId: "user0", IsSensitive: boolPtr(true), @@ -196,7 +195,6 @@ func TestWriteCustomResourceData(t *testing.T) { assert.Equal(t, configurationVariable.Name, d.Get("name")) assert.Equal(t, configurationVariable.Description, d.Get("description")) assert.Equal(t, "terraform", d.Get("type")) - assert.Equal(t, configurationVariable.Value, d.Get("value")) assert.Equal(t, string(configurationVariable.Scope), d.Get("scope")) assert.Equal(t, *configurationVariable.IsReadOnly, d.Get("is_read_only")) assert.Equal(t, *configurationVariable.IsRequired, d.Get("is_required")) @@ -287,7 +285,6 @@ func TestWriteResourceDataSliceVariablesConfigurationVariable(t *testing.T) { Id: "id0", Name: "name0", Description: "desc0", - Value: "v1", Schema: &schema1, } @@ -295,7 +292,6 @@ func TestWriteResourceDataSliceVariablesConfigurationVariable(t *testing.T) { Id: "id1", Name: "name1", Description: "desc1", - Value: "v2", Schema: &schema2, } @@ -305,8 +301,6 @@ func TestWriteResourceDataSliceVariablesConfigurationVariable(t *testing.T) { assert.Equal(t, var1.Name, d.Get("variables.0.name")) assert.Equal(t, var2.Name, d.Get("variables.1.name")) - assert.Equal(t, var1.Value, d.Get("variables.0.value")) - assert.Equal(t, var2.Value, d.Get("variables.1.value")) assert.Equal(t, string(var1.Schema.Format), d.Get("variables.0.format")) assert.Equal(t, string(var2.Schema.Format), d.Get("variables.1.format")) } diff --git a/tests/harness.go b/tests/harness.go index 677077df..7d8c6eda 100644 --- a/tests/harness.go +++ b/tests/harness.go @@ -39,7 +39,7 @@ func main() { } else { success, err := runTest(testName, destroyMode != "NO_DESTROY") if !success { - log.Fatalf("Halting due to test '%s' failure: %s\n", testName, err.Error()) + log.Fatalf("Halting due to test '%s' failure: %s\n", testName, err) } }