From f8e8343910bba806802dbdd014522e35def36e44 Mon Sep 17 00:00:00 2001 From: Tomer Heber Date: Wed, 29 May 2024 09:31:56 -0500 Subject: [PATCH] Feat: add the resource configuration_set (#859) * Feat: add the resource configuration_set - WIP * added more tests * add integration test * pass organization id * fix test * added update and more tests - wip * added more tests * fix test and example * fix test and example * add scope id --- client/configuration_set.go | 16 +- client/configuration_set_test.go | 7 +- env0/provider.go | 1 + env0/resource_variable_set.go | 451 ++++++++++++ env0/resource_variable_set_test.go | 678 ++++++++++++++++++ .../resources/env0_variable_set/resource.tf | 56 ++ tests/integration/033_variable_set/conf.tf | 15 + .../033_variable_set/expected_outputs.json | 1 + tests/integration/033_variable_set/main.tf | 72 ++ 9 files changed, 1292 insertions(+), 5 deletions(-) create mode 100644 env0/resource_variable_set.go create mode 100644 env0/resource_variable_set_test.go create mode 100644 examples/resources/env0_variable_set/resource.tf create mode 100644 tests/integration/033_variable_set/conf.tf create mode 100644 tests/integration/033_variable_set/expected_outputs.json create mode 100644 tests/integration/033_variable_set/main.tf diff --git a/client/configuration_set.go b/client/configuration_set.go index 242ddcbe..b0fbcbbe 100644 --- a/client/configuration_set.go +++ b/client/configuration_set.go @@ -1,18 +1,20 @@ package client +import "fmt" + type CreateConfigurationSetPayload struct { Name string `json:"name"` Description string `json:"description"` // if Scope is "organization", scopeId will be calculated in the functions. Scope string `json:"scope"` // "project" or "organization". ScopeId string `json:"scopeId"` // project id or organization id. - ConfigurationProperties []ConfigurationVariable `json:"configurationProperties"` + ConfigurationProperties []ConfigurationVariable `json:"configurationProperties" tfschema:"-"` } type UpdateConfigurationSetPayload struct { Name string `json:"name"` Description string `json:"description"` - ConfigurationPropertiesChanges []ConfigurationVariable `json:"configurationPropertiesChanges"` // delta changes. + ConfigurationPropertiesChanges []ConfigurationVariable `json:"configurationPropertiesChanges" tfschema:"-"` // delta changes. } type ConfigurationSet struct { @@ -25,7 +27,7 @@ func (client *ApiClient) ConfigurationSetCreate(payload *CreateConfigurationSetP var result ConfigurationSet var err error - if payload.Scope == "organization" { + if payload.Scope == "organization" && payload.ScopeId == "" { payload.ScopeId, err = client.OrganizationId() if err != nil { return nil, err @@ -64,10 +66,16 @@ func (client *ApiClient) ConfigurationSetDelete(id string) error { } func (client *ApiClient) ConfigurationVariablesBySetId(setId string) ([]ConfigurationVariable, error) { + organizationId, err := client.OrganizationId() + if err != nil { + return nil, fmt.Errorf("failed to get organization id: %w", err) + } + var result []ConfigurationVariable if err := client.http.Get("/configuration", map[string]string{ - "setId": setId, + "setId": setId, + "organizationId": organizationId, }, &result); err != nil { return nil, err } diff --git a/client/configuration_set_test.go b/client/configuration_set_test.go index f994b508..562f472f 100644 --- a/client/configuration_set_test.go +++ b/client/configuration_set_test.go @@ -136,9 +136,12 @@ var _ = Describe("Configuration Set", func() { var variables []ConfigurationVariable BeforeEach(func() { + mockOrganizationIdCall(organizationId) + httpCall = mockHttpClient.EXPECT(). Get("/configuration", map[string]string{ - "setId": id, + "setId": id, + "organizationId": organizationId, }, gomock.Any()). Do(func(path string, request interface{}, response *[]ConfigurationVariable) { *response = mockVariables @@ -152,3 +155,5 @@ var _ = Describe("Configuration Set", func() { }) }) }) + +// TODO add more tests... diff --git a/env0/provider.go b/env0/provider.go index 681b29fe..4ea1b1dc 100644 --- a/env0/provider.go +++ b/env0/provider.go @@ -156,6 +156,7 @@ func Provider(version string) plugin.ProviderFunc { "env0_aws_eks_credentials": resourceAwsEksCredentials(), "env0_azure_aks_credentials": resourceAzureAksCredentials(), "env0_gcp_gke_credentials": resourceGcpGkeCredentials(), + "env0_variable_set": resourceVariableSet(), }, } diff --git a/env0/resource_variable_set.go b/env0/resource_variable_set.go new file mode 100644 index 00000000..f706ffe7 --- /dev/null +++ b/env0/resource_variable_set.go @@ -0,0 +1,451 @@ +package env0 + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/env0/terraform-provider-env0/client" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var variableSetVariableSchema *schema.Resource = &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Description: "variable name", + Required: true, + }, + "value": { + Type: schema.TypeString, + Description: "variable value for 'hcl', 'json', or 'text' format", + Optional: true, + }, + "dropdown_values": { + Type: schema.TypeList, + Description: "a list of variable values for 'dropdown' format", + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "type": { + Type: schema.TypeString, + Description: "variable type: terraform or environment (defaults to 'environment')", + Default: "environment", + Optional: true, + ValidateDiagFunc: NewStringInValidator([]string{"environment", "terraform"}), + }, + "is_sensitive": { + Type: schema.TypeBool, + Description: "is the value sensitive (defaults to 'false'). Note: 'dropdown' value format cannot be senstive.", + Optional: true, + Default: false, + }, + "format": { + Type: schema.TypeString, + Description: "the value format: 'text' (free text), 'dropdown' (dropdown list), 'hcl', 'json'. Note: 'hcl' and 'json' can only be used in terraform variables.", + Optional: true, + Default: "text", + ValidateDiagFunc: NewStringInValidator([]string{"text", "dropdown", "hcl", "json"}), + }, + }, +} + +func resourceVariableSet() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceVariableSetCreate, + ReadContext: resourceVariableSetRead, + UpdateContext: resourceVariableSetUpdate, + DeleteContext: resourceVariableSetDelete, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Description: "the name of the variable set", + Required: true, + }, + "description": { + Type: schema.TypeString, + Description: "the description of the variable set", + Optional: true, + }, + "scope": { + Type: schema.TypeString, + Description: "the scope of the variable set: 'organization', or 'project' (defaults to 'organization')", + Optional: true, + Default: "organization", + ValidateDiagFunc: NewStringInValidator([]string{"organization", "project"}), + ForceNew: true, + }, + "scope_id": { + Type: schema.TypeString, + Description: "the scope id (e.g. project id). Note: not required for organization scope.", + Optional: true, + ForceNew: true, + }, + "variable": { + Type: schema.TypeList, + Description: "terraform or environment variable", + Optional: true, + Elem: variableSetVariableSchema, + }, + }, + } +} + +func getVariableFromSchema(d map[string]interface{}) (*client.ConfigurationVariable, error) { + var res client.ConfigurationVariable + + res.Scope = "SET" + res.Name = d["name"].(string) + + isSensitive, ok := d["is_sensitive"].(bool) + if !ok { + isSensitive = false + } + res.IsSensitive = boolPtr(isSensitive) + + variableType := d["type"].(string) + if variableType == "terraform" { + res.Type = (*client.ConfigurationVariableType)(intPtr(1)) + } else { + res.Type = (*client.ConfigurationVariableType)(intPtr(0)) + } + + value, ok := d["value"].(string) + if !ok { + value = "" + } else { + res.Value = value + } + + switch format := d["format"].(string); format { + case "text": + if len(value) == 0 { + return nil, fmt.Errorf("free text variable '%s' must have a value", res.Name) + } + res.Schema = &client.ConfigurationVariableSchema{ + Type: "string", + } + case "hcl": + if len(value) == 0 { + return nil, fmt.Errorf("hcl variable '%s' must have a value", res.Name) + } + res.Schema = &client.ConfigurationVariableSchema{ + Format: "HCL", + } + case "json": + if len(value) == 0 { + return nil, fmt.Errorf("json variable '%s' must have a value", res.Name) + } + res.Schema = &client.ConfigurationVariableSchema{ + Format: "JSON", + } + // validate JSON. + var js json.RawMessage + if err := json.Unmarshal([]byte(value), &js); err != nil { + return nil, fmt.Errorf("json variable '%s' is not a valid json value: %w", res.Name, err) + } + case "dropdown": + ivalues, ok := d["dropdown_values"].([]interface{}) + if !ok || len(ivalues) == 0 { + return nil, fmt.Errorf("dropdown variable '%s' must have dropdown_values", res.Name) + } + + var values []string + for _, ivalue := range ivalues { + values = append(values, ivalue.(string)) + } + + res.Value = ivalues[0].(string) + res.Schema = &client.ConfigurationVariableSchema{ + Type: "string", + Enum: values, + } + } + + return &res, nil +} + +func getSchemaFromVariables(variables []client.ConfigurationVariable) (interface{}, error) { + res := make([]interface{}, 0) + + for _, variable := range variables { + ivariable := make(map[string]interface{}) + res = append(res, ivariable) + + ivariable["name"] = variable.Name + if len(variable.Description) > 0 { + ivariable["description"] = variable.Description + } + + if variable.Type == nil || *variable.Type == client.ConfigurationVariableTypeEnvironment { + ivariable["type"] = "environment" + } else { + ivariable["type"] = "terraform" + } + + if variable.IsSensitive == nil || !*variable.IsSensitive { + ivariable["is_sensitive"] = false + } else { + ivariable["is_sensitive"] = true + } + + if variable.Schema.Type == "string" { + if len(variable.Schema.Enum) > 0 { + ivariable["format"] = "dropdown" + ivalues := make([]interface{}, 0) + for _, value := range variable.Schema.Enum { + ivalues = append(ivalues, value) + } + ivariable["dropdown_values"] = ivalues + } else { + ivariable["format"] = "text" + ivariable["value"] = variable.Value + } + } else if variable.Schema.Format == "HCL" { + ivariable["format"] = "hcl" + ivariable["value"] = variable.Value + } else if variable.Schema.Format == "JSON" { + ivariable["format"] = "json" + ivariable["value"] = variable.Value + } else { + return nil, fmt.Errorf("unhandled variable use-case: %s", variable.Name) + } + } + + return res, nil +} + +func getVariablesFromSchema(d *schema.ResourceData, organizationId string) ([]client.ConfigurationVariable, error) { + res := []client.ConfigurationVariable{} + + ivariables, ok := d.GetOk("variable") + if !ok { + return res, nil + } + + for _, ivariable := range ivariables.([]interface{}) { + variable, err := getVariableFromSchema(ivariable.(map[string]interface{})) + if err != nil { + return nil, err + } + variable.OrganizationId = organizationId + res = append(res, *variable) + } + + return res, nil +} + +func resourceVariableSetCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var err error + + apiClient := meta.(client.ApiClientInterface) + + organizationId, err := apiClient.OrganizationId() + if err != nil { + return diag.Errorf("failed to get organization id") + } + + var payload client.CreateConfigurationSetPayload + if err := readResourceData(&payload, d); err != nil { + return diag.Errorf("schema resource data deserialization failed: %v", err) + } + + if payload.Scope != "organization" && payload.ScopeId == "" { + return diag.Errorf("scope_id must be configured for the scope '%s'", payload.Scope) + } + + if payload.ConfigurationProperties, err = getVariablesFromSchema(d, organizationId); err != nil { + return diag.Errorf("failed to get variables from schema: %v", err) + } + + configurationSet, err := apiClient.ConfigurationSetCreate(&payload) + if err != nil { + return diag.Errorf("failed to create a variable set: %v", err) + } + + d.SetId(configurationSet.Id) + + return nil +} + +func resourceVariableSetDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(client.ApiClientInterface) + + id := d.Id() + + if err := apiClient.ConfigurationSetDelete(id); err != nil { + return diag.Errorf("failed to delete a variable set: %v", err) + } + + return nil +} + +type mergedVariables struct { + currentVariables []client.ConfigurationVariable + newVariables []client.ConfigurationVariable + deletedVariables []client.ConfigurationVariable +} + +func mergeVariables(schema []client.ConfigurationVariable, api []client.ConfigurationVariable) *mergedVariables { + var res mergedVariables + + // To avoid false drifts, keep the order of the 'currentVariables' list similiar to the schema as much as possible. + for _, svariable := range schema { + found := false + + for _, avariable := range api { + if svariable.Name == avariable.Name && *svariable.Type == *avariable.Type { + found = true + if avariable.IsSensitive != nil && *avariable.IsSensitive { + // Senstive - to avoid drift use the value from the schema + avariable.Value = svariable.Value + } + res.currentVariables = append(res.currentVariables, avariable) + break + } + } + + if !found { + // found a variable in the schema but not in the api - this is a new variable. + res.newVariables = append(res.newVariables, svariable) + } + } + + for _, avariable := range api { + found := false + + for _, svariable := range schema { + if svariable.Name == avariable.Name && *svariable.Type == *avariable.Type { + found = true + break + } + } + + if !found { + // found a variable in the api but not in the schema - this is a deleted variable. + res.deletedVariables = append(res.deletedVariables, avariable) + } + } + + return &res +} + +func resourceVariableSetRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(client.ApiClientInterface) + + id := d.Id() + + configurationSet, err := apiClient.ConfigurationSet(id) + if err != nil { + return ResourceGetFailure(ctx, "variable_set", d, err) + } + + if err := writeResourceData(configurationSet, d); err != nil { + return diag.Errorf("schema resource data serialization failed: %v", err) + } + + variablesFromApi, err := apiClient.ConfigurationVariablesBySetId(id) + if err != nil { + return diag.Errorf("failed to get variables from the variables set: %v", err) + } + + variablesFromSchema, err := getVariablesFromSchema(d, "") + if err != nil { + return diag.Errorf("failed to get variables from schema: %v", err) + } + + mergedVariables := mergeVariables(variablesFromSchema, variablesFromApi) + + // for "READ" the source of truth is the variables from the API - existing + deleted. + variables := append(mergedVariables.currentVariables, mergedVariables.deletedVariables...) + + ivariables, err := getSchemaFromVariables(variables) + if err != nil { + return diag.Errorf("failed to get schema from variables: %v", err) + } + + if err := d.Set("variable", ivariables); err != nil { + return diag.Errorf("failed to set variables in schema: %v", err) + } + + return nil +} + +func resourceVariableSetUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var err error + + id := d.Id() + + apiClient := meta.(client.ApiClientInterface) + + organizationId, err := apiClient.OrganizationId() + if err != nil { + return diag.Errorf("failed to get organization id") + } + + var payload client.UpdateConfigurationSetPayload + if err := readResourceData(&payload, d); err != nil { + return diag.Errorf("schema resource data deserialization failed: %v", err) + } + + variablesFromApi, err := apiClient.ConfigurationVariablesBySetId(id) + if err != nil { + return diag.Errorf("failed to get variables from the variables set: %v", err) + } + + variablesFromSchema, err := getVariablesFromSchema(d, organizationId) + if err != nil { + return diag.Errorf("failed to get variables from schema: %v", err) + } + + mergedVariables := mergeVariables(variablesFromSchema, variablesFromApi) + + // Any new variables should be added to the delta. + payload.ConfigurationPropertiesChanges = append(payload.ConfigurationPropertiesChanges, mergedVariables.newVariables...) + + // Any deleted variables should be added to the delta. + for _, deletedVariable := range mergedVariables.deletedVariables { + deletedVariable.ToDelete = boolPtr(true) + payload.ConfigurationPropertiesChanges = append(payload.ConfigurationPropertiesChanges, deletedVariable) + } + + // Check if existing variables have changed. If they have add them to the delta as well. + for i, svariable := range variablesFromSchema { + if !d.HasChange(fmt.Sprintf("variable.%d", i)) { + continue + } + + for _, cvariable := range mergedVariables.currentVariables { + if svariable.Name == cvariable.Name && *svariable.Type == *cvariable.Type { + // an existing variable has changed - add it the delta. + svariable.OrganizationId = organizationId + svariable.ScopeId = id + svariable.Scope = "SET" + + // if the format hasn't changed - use existing id. Otherwise, keep id empty. + if !d.HasChange(fmt.Sprintf("variable.%d.format", i)) { + svariable.Id = cvariable.Id + } + + payload.ConfigurationPropertiesChanges = append(payload.ConfigurationPropertiesChanges, svariable) + + break + } + } + } + + // Make sure all changes have a scopeId (otherwise the update request will fail). + for i := range payload.ConfigurationPropertiesChanges { + payload.ConfigurationPropertiesChanges[i].ScopeId = id + } + + if _, err := apiClient.ConfigurationSetUpdate(id, &payload); err != nil { + return diag.Errorf("failed to update a variable set: %v", err) + } + + return nil +} diff --git a/env0/resource_variable_set_test.go b/env0/resource_variable_set_test.go new file mode 100644 index 00000000..4421eb7f --- /dev/null +++ b/env0/resource_variable_set_test.go @@ -0,0 +1,678 @@ +package env0 + +import ( + "fmt" + "regexp" + "testing" + + "github.com/env0/terraform-provider-env0/client" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "go.uber.org/mock/gomock" +) + +func TestUnitVariableSetResource(t *testing.T) { + resourceType := "env0_variable_set" + resourceName := "test" + accessor := resourceAccessor(resourceType, resourceName) + + organizationId := "org" + projectId := "proj" + + configurationSet := client.ConfigurationSet{ + Id: "idddd111", + Name: "name1", + Description: "des1", + } + + textVariable := client.ConfigurationVariable{ + Name: "nv1", + Value: "v1", + OrganizationId: organizationId, + IsSensitive: boolPtr(false), + Scope: "SET", + Type: (*client.ConfigurationVariableType)(intPtr(1)), + Schema: &client.ConfigurationVariableSchema{ + Type: "string", + }, + } + + textVariableWithScopeId := textVariable + textVariableWithScopeId.Id = "idtextvariable" + textVariableWithScopeId.ScopeId = configurationSet.Id + + sensitiveTextVariable := client.ConfigurationVariable{ + Name: "nv1", + Value: "v2", + OrganizationId: organizationId, + IsSensitive: boolPtr(true), + Scope: "SET", + Type: (*client.ConfigurationVariableType)(intPtr(0)), + Schema: &client.ConfigurationVariableSchema{ + Type: "string", + }, + } + + sensitiveTextVariableWithScopeId := sensitiveTextVariable + sensitiveTextVariableWithScopeId.Id = "idsentivetextvariable" + sensitiveTextVariableWithScopeId.ScopeId = configurationSet.Id + sensitiveTextVariableWithScopeId.Value = "OMITTED" + + hclVariable := client.ConfigurationVariable{ + Name: "hcl1", + Value: "sdzdfsdfsd", + OrganizationId: organizationId, + IsSensitive: boolPtr(false), + Scope: "SET", + Type: (*client.ConfigurationVariableType)(intPtr(1)), + Schema: &client.ConfigurationVariableSchema{ + Format: "HCL", + }, + } + + hclVariableWithScopeId := hclVariable + hclVariableWithScopeId.Id = "idhclvariable" + hclVariableWithScopeId.ScopeId = configurationSet.Id + + jsonVariable := client.ConfigurationVariable{ + Name: "json1", + Value: "{}", + OrganizationId: organizationId, + IsSensitive: boolPtr(false), + Scope: "SET", + Type: (*client.ConfigurationVariableType)(intPtr(1)), + Schema: &client.ConfigurationVariableSchema{ + Format: "JSON", + }, + } + + jsonVariableWithScopeId := jsonVariable + jsonVariableWithScopeId.Id = "idjsonvariable" + jsonVariableWithScopeId.ScopeId = configurationSet.Id + + dropdownVariable := client.ConfigurationVariable{ + Name: "dropdown123", + Value: "o1", + OrganizationId: organizationId, + IsSensitive: boolPtr(false), + Scope: "SET", + Type: (*client.ConfigurationVariableType)(intPtr(1)), + Schema: &client.ConfigurationVariableSchema{ + Type: "string", + Enum: []string{ + "o1", "o2", + }, + }, + } + + dropdownVariableWithScopeId := dropdownVariable + dropdownVariableWithScopeId.Id = "iddropdownvariable" + dropdownVariableWithScopeId.ScopeId = configurationSet.Id + + t.Run("basic - organization scope", func(t *testing.T) { + createPayload := client.CreateConfigurationSetPayload{ + Name: configurationSet.Name, + Description: configurationSet.Description, + Scope: "organization", + ConfigurationProperties: []client.ConfigurationVariable{ + textVariable, + sensitiveTextVariable, + hclVariable, + jsonVariable, + dropdownVariable, + }, + } + + createTestCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "%s" "%s" { + name = "%s" + description = "%s" + + variable { + name = "%s" + value = "%s" + type = "terraform" + format = "text" + } + + variable { + name = "%s" + value = "%s" + format = "text" + is_sensitive = true + } + + variable { + name = "%s" + value = "%s" + type = "terraform" + format = "hcl" + } + + variable { + name = "%s" + value = "%s" + type = "terraform" + format = "json" + } + + variable { + name = "%s" + dropdown_values = ["o1", "o2"] + type = "terraform" + format = "dropdown" + } + }`, resourceType, resourceName, createPayload.Name, createPayload.Description, + textVariable.Name, textVariable.Value, + sensitiveTextVariable.Name, sensitiveTextVariable.Value, + hclVariable.Name, hclVariable.Value, + jsonVariable.Name, jsonVariable.Value, + dropdownVariable.Name, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", configurationSet.Id), + resource.TestCheckResourceAttr(accessor, "name", configurationSet.Name), + resource.TestCheckResourceAttr(accessor, "description", configurationSet.Description), + resource.TestCheckResourceAttr(accessor, "scope", "organization"), + resource.TestCheckResourceAttr(accessor, "variable.0.value", textVariable.Value), + resource.TestCheckResourceAttr(accessor, "variable.0.name", textVariable.Name), + resource.TestCheckResourceAttr(accessor, "variable.0.type", "terraform"), + resource.TestCheckResourceAttr(accessor, "variable.0.format", "text"), + resource.TestCheckResourceAttr(accessor, "variable.1.value", sensitiveTextVariable.Value), + resource.TestCheckResourceAttr(accessor, "variable.1.name", sensitiveTextVariable.Name), + resource.TestCheckResourceAttr(accessor, "variable.1.type", "environment"), + resource.TestCheckResourceAttr(accessor, "variable.1.format", "text"), + resource.TestCheckResourceAttr(accessor, "variable.1.is_sensitive", "true"), + resource.TestCheckResourceAttr(accessor, "variable.2.value", hclVariable.Value), + resource.TestCheckResourceAttr(accessor, "variable.2.name", hclVariable.Name), + resource.TestCheckResourceAttr(accessor, "variable.2.type", "terraform"), + resource.TestCheckResourceAttr(accessor, "variable.2.format", "hcl"), + resource.TestCheckResourceAttr(accessor, "variable.3.value", jsonVariable.Value), + resource.TestCheckResourceAttr(accessor, "variable.3.name", jsonVariable.Name), + resource.TestCheckResourceAttr(accessor, "variable.3.type", "terraform"), + resource.TestCheckResourceAttr(accessor, "variable.3.format", "json"), + resource.TestCheckResourceAttr(accessor, "variable.4.dropdown_values.0", dropdownVariable.Schema.Enum[0]), + resource.TestCheckResourceAttr(accessor, "variable.4.dropdown_values.1", dropdownVariable.Schema.Enum[1]), + resource.TestCheckResourceAttr(accessor, "variable.4.name", dropdownVariable.Name), + resource.TestCheckResourceAttr(accessor, "variable.4.type", "terraform"), + resource.TestCheckResourceAttr(accessor, "variable.4.format", "dropdown"), + ), + }, + }, + } + + runUnitTest(t, createTestCase, func(mock *client.MockApiClientInterface) { + mock.EXPECT().OrganizationId().AnyTimes().Return(organizationId, nil) + + gomock.InOrder( + mock.EXPECT().ConfigurationSetCreate(&createPayload).Times(1).Return(&configurationSet, nil), + mock.EXPECT().ConfigurationSet(configurationSet.Id).Times(1).Return(&configurationSet, nil), + mock.EXPECT().ConfigurationVariablesBySetId(configurationSet.Id).Times(1).Return([]client.ConfigurationVariable{ + textVariableWithScopeId, + sensitiveTextVariableWithScopeId, + hclVariableWithScopeId, + jsonVariableWithScopeId, + dropdownVariableWithScopeId, + }, nil), + mock.EXPECT().ConfigurationSetDelete(configurationSet.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("basic - project scope", func(t *testing.T) { + createPayload := client.CreateConfigurationSetPayload{ + Name: configurationSet.Name, + Description: configurationSet.Description, + Scope: "project", + ScopeId: projectId, + ConfigurationProperties: []client.ConfigurationVariable{ + textVariable, + }, + } + + createTestCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "%s" "%s" { + name = "%s" + description = "%s" + scope = "project" + scope_id = "%s" + + variable { + name = "%s" + value = "%s" + type = "terraform" + format = "text" + } + }`, resourceType, resourceName, createPayload.Name, createPayload.Description, createPayload.ScopeId, + textVariable.Name, textVariable.Value, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", configurationSet.Id), + resource.TestCheckResourceAttr(accessor, "name", configurationSet.Name), + resource.TestCheckResourceAttr(accessor, "description", configurationSet.Description), + resource.TestCheckResourceAttr(accessor, "scope", "project"), + resource.TestCheckResourceAttr(accessor, "scope_id", projectId), + resource.TestCheckResourceAttr(accessor, "variable.0.value", textVariable.Value), + resource.TestCheckResourceAttr(accessor, "variable.0.name", textVariable.Name), + resource.TestCheckResourceAttr(accessor, "variable.0.type", "terraform"), + resource.TestCheckResourceAttr(accessor, "variable.0.format", "text"), + ), + }, + }, + } + + runUnitTest(t, createTestCase, func(mock *client.MockApiClientInterface) { + mock.EXPECT().OrganizationId().AnyTimes().Return(organizationId, nil) + + gomock.InOrder( + mock.EXPECT().ConfigurationSetCreate(&createPayload).Times(1).Return(&configurationSet, nil), + mock.EXPECT().ConfigurationSet(configurationSet.Id).Times(1).Return(&configurationSet, nil), + mock.EXPECT().ConfigurationVariablesBySetId(configurationSet.Id).Times(1).Return([]client.ConfigurationVariable{ + textVariableWithScopeId, + }, nil), + mock.EXPECT().ConfigurationSetDelete(configurationSet.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("update", func(t *testing.T) { + createPayload := client.CreateConfigurationSetPayload{ + Name: configurationSet.Name, + Description: configurationSet.Description, + Scope: "organization", + ConfigurationProperties: []client.ConfigurationVariable{ + textVariable, + }, + } + + updatedTextVariable := textVariable + updatedTextVariable.Value = "new-value" + + updatedTextVariableWithScopeId := textVariableWithScopeId + updatedTextVariableWithScopeId.Value = updatedTextVariable.Value + + updatedDropDownVariable := dropdownVariable + updatedDropDownVariable.Name = updatedTextVariable.Name + updatedDropDownVariable.Id = "" + updatedDropDownVariable.ScopeId = configurationSet.Id + + updatedDropDownVariableRes := dropdownVariableWithScopeId + updatedDropDownVariableRes.Name = updatedDropDownVariable.Name + + dropdownVariable2 := updatedDropDownVariableRes + dropdownVariable2.Value = "o3" + schema := *dropdownVariable2.Schema + dropdownVariable2.Schema = &schema + dropdownVariable2.Schema.Enum = []string{"o3", "o2"} + + deleteDropdownVariable2 := dropdownVariable2 + deleteDropdownVariable2.ToDelete = boolPtr(true) + + updateTestCase := resource.TestCase{ + // Create a text variable. + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "%s" "%s" { + name = "%s" + description = "%s" + + variable { + name = "%s" + value = "%s" + type = "terraform" + format = "text" + } + }`, resourceType, resourceName, configurationSet.Name, configurationSet.Description, + textVariable.Name, textVariable.Value, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", configurationSet.Id), + resource.TestCheckResourceAttr(accessor, "name", configurationSet.Name), + resource.TestCheckResourceAttr(accessor, "description", configurationSet.Description), + resource.TestCheckResourceAttr(accessor, "scope", "organization"), + resource.TestCheckResourceAttr(accessor, "variable.0.value", textVariable.Value), + resource.TestCheckResourceAttr(accessor, "variable.0.name", textVariable.Name), + resource.TestCheckResourceAttr(accessor, "variable.0.type", "terraform"), + resource.TestCheckResourceAttr(accessor, "variable.0.format", "text"), + ), + }, + // Update the value of a text variable. + { + Config: fmt.Sprintf(` + resource "%s" "%s" { + name = "%s" + description = "%s" + + variable { + name = "%s" + value = "%s" + type = "terraform" + format = "text" + } + }`, resourceType, resourceName, configurationSet.Name, configurationSet.Description, + updatedTextVariable.Name, updatedTextVariable.Value, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", configurationSet.Id), + resource.TestCheckResourceAttr(accessor, "name", configurationSet.Name), + resource.TestCheckResourceAttr(accessor, "description", configurationSet.Description), + resource.TestCheckResourceAttr(accessor, "scope", "organization"), + resource.TestCheckResourceAttr(accessor, "variable.0.value", updatedTextVariable.Value), + resource.TestCheckResourceAttr(accessor, "variable.0.name", textVariable.Name), + resource.TestCheckResourceAttr(accessor, "variable.0.type", "terraform"), + resource.TestCheckResourceAttr(accessor, "variable.0.format", "text"), + ), + }, + // Switch the variable format - will update without an id. + { + Config: fmt.Sprintf(` + resource "%s" "%s" { + name = "%s" + description = "%s" + + variable { + name = "%s" + dropdown_values = ["o1", "o2"] + type = "terraform" + format = "dropdown" + } + }`, resourceType, resourceName, configurationSet.Name, configurationSet.Description, + updatedDropDownVariable.Name, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", configurationSet.Id), + resource.TestCheckResourceAttr(accessor, "name", configurationSet.Name), + resource.TestCheckResourceAttr(accessor, "description", configurationSet.Description), + resource.TestCheckResourceAttr(accessor, "scope", "organization"), + resource.TestCheckResourceAttr(accessor, "variable.0.dropdown_values.0", dropdownVariable.Schema.Enum[0]), + resource.TestCheckResourceAttr(accessor, "variable.0.dropdown_values.1", dropdownVariable.Schema.Enum[1]), + resource.TestCheckResourceAttr(accessor, "variable.0.name", textVariable.Name), + resource.TestCheckResourceAttr(accessor, "variable.0.type", "terraform"), + resource.TestCheckResourceAttr(accessor, "variable.0.format", "dropdown"), + ), + }, + // Update the dropdown values. + { + Config: fmt.Sprintf(` + resource "%s" "%s" { + name = "%s" + description = "%s" + + variable { + name = "%s" + dropdown_values = ["o3", "o2"] + type = "terraform" + format = "dropdown" + } + }`, resourceType, resourceName, configurationSet.Name, configurationSet.Description, + textVariable.Name, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", configurationSet.Id), + resource.TestCheckResourceAttr(accessor, "name", configurationSet.Name), + resource.TestCheckResourceAttr(accessor, "description", configurationSet.Description), + resource.TestCheckResourceAttr(accessor, "scope", "organization"), + resource.TestCheckResourceAttr(accessor, "variable.0.dropdown_values.0", dropdownVariable2.Schema.Enum[0]), + resource.TestCheckResourceAttr(accessor, "variable.0.dropdown_values.1", dropdownVariable2.Schema.Enum[1]), + resource.TestCheckResourceAttr(accessor, "variable.0.name", textVariable.Name), + resource.TestCheckResourceAttr(accessor, "variable.0.type", "terraform"), + resource.TestCheckResourceAttr(accessor, "variable.0.format", "dropdown"), + ), + }, + // remove variable. + { + Config: fmt.Sprintf(` + resource "%s" "%s" { + name = "%s" + description = "%s" + }`, resourceType, resourceName, configurationSet.Name, configurationSet.Description, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", configurationSet.Id), + resource.TestCheckResourceAttr(accessor, "name", configurationSet.Name), + resource.TestCheckResourceAttr(accessor, "description", configurationSet.Description), + resource.TestCheckResourceAttr(accessor, "scope", "organization"), + ), + }, + }, + } + + runUnitTest(t, updateTestCase, func(mock *client.MockApiClientInterface) { + mock.EXPECT().OrganizationId().AnyTimes().Return(organizationId, nil) + mock.EXPECT().ConfigurationSet(configurationSet.Id).AnyTimes().Return(&configurationSet, nil) + + gomock.InOrder( + mock.EXPECT().ConfigurationSetCreate(&createPayload).Times(1).Return(&configurationSet, nil), + mock.EXPECT().ConfigurationVariablesBySetId(configurationSet.Id).Times(3).Return([]client.ConfigurationVariable{ + textVariableWithScopeId, + }, nil), + mock.EXPECT().ConfigurationSetUpdate(configurationSet.Id, &client.UpdateConfigurationSetPayload{ + Name: configurationSet.Name, + Description: configurationSet.Description, + ConfigurationPropertiesChanges: []client.ConfigurationVariable{updatedTextVariableWithScopeId}, + }).Times(1).Return(&configurationSet, nil), + mock.EXPECT().ConfigurationVariablesBySetId(configurationSet.Id).Times(3).Return([]client.ConfigurationVariable{ + updatedTextVariableWithScopeId, + }, nil), + mock.EXPECT().ConfigurationSetUpdate(configurationSet.Id, &client.UpdateConfigurationSetPayload{ + Name: configurationSet.Name, + Description: configurationSet.Description, + ConfigurationPropertiesChanges: []client.ConfigurationVariable{updatedDropDownVariable}, + }).Times(1).Return(&configurationSet, nil), + mock.EXPECT().ConfigurationVariablesBySetId(configurationSet.Id).Times(3).Return([]client.ConfigurationVariable{ + updatedDropDownVariableRes, + }, nil), + mock.EXPECT().ConfigurationSetUpdate(configurationSet.Id, &client.UpdateConfigurationSetPayload{ + Name: configurationSet.Name, + Description: configurationSet.Description, + ConfigurationPropertiesChanges: []client.ConfigurationVariable{dropdownVariable2}, + }).Times(1).Return(&configurationSet, nil), + mock.EXPECT().ConfigurationVariablesBySetId(configurationSet.Id).Times(3).Return([]client.ConfigurationVariable{ + dropdownVariable2, + }, nil), + mock.EXPECT().ConfigurationSetUpdate(configurationSet.Id, &client.UpdateConfigurationSetPayload{ + Name: configurationSet.Name, + Description: configurationSet.Description, + ConfigurationPropertiesChanges: []client.ConfigurationVariable{deleteDropdownVariable2}, + }).Times(1).Return(&configurationSet, nil), + mock.EXPECT().ConfigurationVariablesBySetId(configurationSet.Id).Times(1).Return([]client.ConfigurationVariable{}, nil), + mock.EXPECT().ConfigurationSetDelete(configurationSet.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("update sensitive variable", func(t *testing.T) { + createPayload := client.CreateConfigurationSetPayload{ + Name: configurationSet.Name, + Description: configurationSet.Description, + Scope: "organization", + ConfigurationProperties: []client.ConfigurationVariable{ + sensitiveTextVariable, + }, + } + + updatedSensitiveTextVariable := sensitiveTextVariableWithScopeId + updatedSensitiveTextVariable.Value = "some new value 1234 sensitive" + + updateTestCase := resource.TestCase{ + // Create a text variable. + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "%s" "%s" { + name = "%s" + description = "%s" + + variable { + name = "%s" + value = "%s" + type = "environment" + format = "text" + is_sensitive = true + } + }`, resourceType, resourceName, configurationSet.Name, configurationSet.Description, + sensitiveTextVariable.Name, sensitiveTextVariable.Value, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", configurationSet.Id), + resource.TestCheckResourceAttr(accessor, "name", configurationSet.Name), + resource.TestCheckResourceAttr(accessor, "description", configurationSet.Description), + resource.TestCheckResourceAttr(accessor, "scope", "organization"), + resource.TestCheckResourceAttr(accessor, "variable.0.value", sensitiveTextVariable.Value), + resource.TestCheckResourceAttr(accessor, "variable.0.name", sensitiveTextVariable.Name), + resource.TestCheckResourceAttr(accessor, "variable.0.type", "environment"), + resource.TestCheckResourceAttr(accessor, "variable.0.format", "text"), + resource.TestCheckResourceAttr(accessor, "variable.0.is_sensitive", "true"), + ), + }, + { + Config: fmt.Sprintf(` + resource "%s" "%s" { + name = "%s" + description = "%s" + + variable { + name = "%s" + value = "%s" + type = "environment" + format = "text" + is_sensitive = true + } + }`, resourceType, resourceName, configurationSet.Name, configurationSet.Description, + sensitiveTextVariable.Name, updatedSensitiveTextVariable.Value, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", configurationSet.Id), + resource.TestCheckResourceAttr(accessor, "name", configurationSet.Name), + resource.TestCheckResourceAttr(accessor, "description", configurationSet.Description), + resource.TestCheckResourceAttr(accessor, "scope", "organization"), + resource.TestCheckResourceAttr(accessor, "variable.0.value", updatedSensitiveTextVariable.Value), + resource.TestCheckResourceAttr(accessor, "variable.0.name", sensitiveTextVariable.Name), + resource.TestCheckResourceAttr(accessor, "variable.0.type", "environment"), + resource.TestCheckResourceAttr(accessor, "variable.0.format", "text"), + resource.TestCheckResourceAttr(accessor, "variable.0.is_sensitive", "true"), + ), + }, + }, + } + + runUnitTest(t, updateTestCase, func(mock *client.MockApiClientInterface) { + mock.EXPECT().OrganizationId().AnyTimes().Return(organizationId, nil) + mock.EXPECT().ConfigurationSet(configurationSet.Id).AnyTimes().Return(&configurationSet, nil) + + gomock.InOrder( + mock.EXPECT().ConfigurationSetCreate(&createPayload).Times(1).Return(&configurationSet, nil), + mock.EXPECT().ConfigurationVariablesBySetId(configurationSet.Id).Times(3).Return([]client.ConfigurationVariable{ + sensitiveTextVariableWithScopeId, + }, nil), + mock.EXPECT().ConfigurationSetUpdate(configurationSet.Id, &client.UpdateConfigurationSetPayload{ + Name: configurationSet.Name, + Description: configurationSet.Description, + ConfigurationPropertiesChanges: []client.ConfigurationVariable{updatedSensitiveTextVariable}, + }).Times(1).Return(&configurationSet, nil), + mock.EXPECT().ConfigurationVariablesBySetId(configurationSet.Id).Times(1).Return([]client.ConfigurationVariable{ + sensitiveTextVariableWithScopeId, + }, nil), + mock.EXPECT().ConfigurationSetDelete(configurationSet.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("failures", func(t *testing.T) { + runFailure := func(testName string, config string, errorMessage string) { + t.Run(testName, func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: config, + ExpectError: regexp.MustCompile(errorMessage), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + mock.EXPECT().OrganizationId().AnyTimes().Return(organizationId, nil) + }) + }) + } + + runFailure("no value - text", fmt.Sprintf(` + resource "%s" "%s" { + name = "fail" + description = "description1" + scope = "organization" + + variable { + name = "a" + type = "terraform" + format = "text" + } + }`, resourceType, resourceName), "free text variable 'a' must have a value") + + runFailure("no value - hcl", fmt.Sprintf(` + resource "%s" "%s" { + name = "fail" + description = "description1" + scope = "organization" + + variable { + name = "a" + type = "terraform" + format = "hcl" + } + }`, resourceType, resourceName), "hcl variable 'a' must have a value") + + runFailure("no value - json", fmt.Sprintf(` + resource "%s" "%s" { + name = "fail" + description = "description1" + scope = "organization" + + variable { + name = "a" + type = "terraform" + format = "json" + } + }`, resourceType, resourceName), "json variable 'a' must have a value") + + runFailure("no dropdown_values", fmt.Sprintf(` + resource "%s" "%s" { + name = "fail" + description = "description1" + scope = "organization" + + variable { + name = "a" + type = "terraform" + format = "dropdown" + } + }`, resourceType, resourceName), "dropdown variable 'a' must have dropdown_values") + + runFailure("invalid json", fmt.Sprintf(` + resource "%s" "%s" { + name = "fail" + description = "description1" + scope = "organization" + + variable { + name = "a" + type = "terraform" + format = "json" + value = "i am not a valid json" + } + }`, resourceType, resourceName), "json variable 'a' is not a valid json value") + + runFailure("project scope with no scope id", fmt.Sprintf(` + resource "%s" "%s" { + name = "fail" + description = "description1" + scope = "project" + }`, resourceType, resourceName), "scope_id must be configured for the scope 'project'") + }) +} diff --git a/examples/resources/env0_variable_set/resource.tf b/examples/resources/env0_variable_set/resource.tf new file mode 100644 index 00000000..0861b6e9 --- /dev/null +++ b/examples/resources/env0_variable_set/resource.tf @@ -0,0 +1,56 @@ +data "env0_project" "project" { + name = "project" +} + +resource "env0_variable_set" "organization_scope_example" { + name = "variable-set-example1" + description = "description123" + + variable { + name = "n1" + value = "v1" + format = "text" + } + + variable { + name = "n1" + value = "v2" + type = "environment" + format = "text" + is_sensitive = true + } + + variable { + name = "n3" + value = "v3" + type = "terraform" + format = "hcl" + } + + variable { + name = "n4" + value = "{}" + type = "terraform" + format = "json" + } + + variable { + name = "n5" + dropdown_values = ["o3", "o2"] + type = "terraform" + format = "dropdown" + } +} + +resource "env0_variable_set" "project_scope_example" { + name = "variable-set-example2" + description = "description123" + scope = "project" + scope_id = data.env0_project.project.id + + variable { + name = "n1" + value = "v1" + format = "text" + } +} diff --git a/tests/integration/033_variable_set/conf.tf b/tests/integration/033_variable_set/conf.tf new file mode 100644 index 00000000..8d6d2954 --- /dev/null +++ b/tests/integration/033_variable_set/conf.tf @@ -0,0 +1,15 @@ +terraform { + backend "local" { + } + required_providers { + env0 = { + source = "terraform-registry.env0.com/env0/env0" + } + } +} + +provider "env0" {} + +variable "second_run" { + default = false +} diff --git a/tests/integration/033_variable_set/expected_outputs.json b/tests/integration/033_variable_set/expected_outputs.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/tests/integration/033_variable_set/expected_outputs.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/tests/integration/033_variable_set/main.tf b/tests/integration/033_variable_set/main.tf new file mode 100644 index 00000000..2bc430ca --- /dev/null +++ b/tests/integration/033_variable_set/main.tf @@ -0,0 +1,72 @@ +provider "random" {} + +resource "random_string" "random" { + length = 8 + special = false + min_lower = 8 +} + +resource "env0_project" "project" { + name = "project-for-variable-set-${random_string.random.result}" +} + +resource "env0_variable_set" "org_scope" { + name = "variable-set-org-${random_string.random.result}" + description = "description123" + + variable { + name = "n1" + value = var.second_run ? "v2" : "v1" + type = "terraform" + format = "text" + } + + variable { + name = "n1" + value = var.second_run ? "v22" : "v2" + format = "text" + is_sensitive = true + } + + variable { + name = "n3" + value = var.second_run ? "v32" : "v3" + type = "terraform" + format = "hcl" + } + + variable { + name = "n4" + value = "{}" + type = "terraform" + format = "json" + } + + variable { + name = "n5" + dropdown_values = var.second_run ? ["o3", "o2"] : ["o1", "o2"] + type = "terraform" + format = "dropdown" + } + + variable { + name = "n55555" + value = "abcdef" + type = var.second_run ? "terraform" : "environment" + format = "text" + } +} + +resource "env0_variable_set" "project_scope" { + name = "variable-set-project-${random_string.random.result}" + description = "description123" + scope = "project" + scope_id = env0_project.project.id + + variable { + name = "n1" + value = "v1" + type = "terraform" + format = "text" + } +}