diff --git a/client/configuration_variable.go b/client/configuration_variable.go index 3578aec7..7c1c0564 100644 --- a/client/configuration_variable.go +++ b/client/configuration_variable.go @@ -44,6 +44,7 @@ type ConfigurationVariable struct { ToDelete *bool `json:"toDelete,omitempty"` IsReadonly *bool `json:"isReadonly,omitempty"` IsRequired *bool `json:"isRequired,omitempty"` + Regex string `json:"regex,omitempty"` } type ConfigurationVariableCreateParams struct { @@ -58,6 +59,7 @@ type ConfigurationVariableCreateParams struct { Format Format IsReadonly bool IsRequired bool + Regex string } type ConfigurationVariableUpdateParams struct { @@ -134,6 +136,7 @@ func (client *ApiClient) ConfigurationVariableCreate(params ConfigurationVariabl "organizationId": organizationId, "isRequired": params.IsRequired, "isReadonly": params.IsReadonly, + "regex": params.Regex, } if params.Scope != ScopeGlobal { request["scopeId"] = params.ScopeId @@ -187,6 +190,7 @@ func (client *ApiClient) ConfigurationVariableUpdate(updateParams ConfigurationV "organizationId": organizationId, "isRequired": commonParams.IsRequired, "isReadonly": commonParams.IsReadonly, + "regex": commonParams.Regex, } if commonParams.Scope != ScopeGlobal { request["scopeId"] = commonParams.ScopeId diff --git a/client/configuration_variable_test.go b/client/configuration_variable_test.go index 6c4c4a9e..28715ded 100644 --- a/client/configuration_variable_test.go +++ b/client/configuration_variable_test.go @@ -35,6 +35,7 @@ var _ = Describe("Configuration Variable", func() { Schema: &schema, IsReadonly: &isReadonly, IsRequired: &isRequired, + Regex: "regex", } mockGlobalConfigurationVariable := ConfigurationVariable{ @@ -51,6 +52,7 @@ var _ = Describe("Configuration Variable", func() { Schema: &schema, IsReadonly: &isReadonly, IsRequired: &isRequired, + Regex: "regex", } Describe("ConfigurationVariable", func() { @@ -123,6 +125,7 @@ var _ = Describe("Configuration Variable", func() { "schema": schema, "isReadonly": *mockConfig.IsReadonly, "isRequired": *mockConfig.IsRequired, + "regex": mockConfig.Regex, }} return request } @@ -150,6 +153,7 @@ var _ = Describe("Configuration Variable", func() { Format: mockConfig.Schema.Format, IsReadonly: *mockConfig.IsReadonly, IsRequired: *mockConfig.IsRequired, + Regex: mockConfig.Regex, }, ) } @@ -225,6 +229,7 @@ var _ = Describe("Configuration Variable", func() { }, "isReadonly": *mockConfigurationVariable.IsReadonly, "isRequired": *mockConfigurationVariable.IsRequired, + "regex": mockConfigurationVariable.Regex, }} httpCall = mockHttpClient.EXPECT(). @@ -247,6 +252,7 @@ var _ = Describe("Configuration Variable", func() { EnumValues: nil, IsReadonly: *mockConfigurationVariable.IsReadonly, IsRequired: *mockConfigurationVariable.IsRequired, + Regex: mockConfigurationVariable.Regex, }, }, ) diff --git a/docs/data-sources/configuration_variable.md b/docs/data-sources/configuration_variable.md index e4c046f7..42d96b5c 100644 --- a/docs/data-sources/configuration_variable.md +++ b/docs/data-sources/configuration_variable.md @@ -35,6 +35,7 @@ output "aws_default_region" { - **is_required** (Boolean) specifies if the value of this variable must be set by lower scopes - **name** (String) the name of the configuration variable - **project_id** (String) search for the variable under this project, not globally +- **regex** (String) specifies a regular expression to validate variable value in UI - **template_id** (String) search for the variable under this template, not globally - **type** (String) 'terraform' or 'environment'. If specified as an argument, limits searching by variable name only to variables of this type. diff --git a/docs/resources/configuration_variable.md b/docs/resources/configuration_variable.md index 0ee94c06..3941d2ab 100644 --- a/docs/resources/configuration_variable.md +++ b/docs/resources/configuration_variable.md @@ -54,6 +54,7 @@ resource "env0_configuration_variable" "json_variable" { - **is_required** (Boolean) the value of this variable must be set by lower scopes - **is_sensitive** (Boolean) is the variable sensitive, defaults to false - **project_id** (String) create the variable under this project, not globally +- **regex** (String) the value of this variable must match provided regular expression - **template_id** (String) create the variable under this template, not globally - **type** (String) default 'environment'. set to 'terraform' to create a terraform variable - **value** (String, Sensitive) value for the configuration variable diff --git a/env0/data_configuration_variable.go b/env0/data_configuration_variable.go index 27dca0f7..02b7c95c 100644 --- a/env0/data_configuration_variable.go +++ b/env0/data_configuration_variable.go @@ -109,6 +109,12 @@ func dataConfigurationVariable() *schema.Resource { Computed: true, Optional: true, }, + "regex": { + Type: schema.TypeString, + Description: "specifies a regular expression to validate variable value (enforced only in env0 UI)", + Computed: true, + Optional: true, + }, }, } } @@ -146,6 +152,7 @@ func dataConfigurationVariableRead(ctx context.Context, d *schema.ResourceData, d.Set("enum", variable.Schema.Enum) d.Set("is_read_only", variable.IsReadonly) d.Set("is_required", variable.IsRequired) + d.Set("regex", variable.Regex) if variable.Schema.Format != client.Text { d.Set("format", string(variable.Schema.Format)) diff --git a/env0/data_configuration_variable_test.go b/env0/data_configuration_variable_test.go index 45e7b6ae..1b3207dd 100644 --- a/env0/data_configuration_variable_test.go +++ b/env0/data_configuration_variable_test.go @@ -33,6 +33,7 @@ func TestUnitConfigurationVariableData(t *testing.T) { Schema: &client.ConfigurationVariableSchema{Type: "string", Format: client.HCL}, IsReadonly: &isReadonly, IsRequired: &isRequired, + Regex: "regex", } checkResources := resource.ComposeAggregateTestCheckFunc( @@ -46,6 +47,7 @@ func TestUnitConfigurationVariableData(t *testing.T) { resource.TestCheckResourceAttr(accessor, "format", string(configurationVariable.Schema.Format)), resource.TestCheckResourceAttr(accessor, "is_read_only", strconv.FormatBool(*configurationVariable.IsReadonly)), resource.TestCheckResourceAttr(accessor, "is_required", strconv.FormatBool(*configurationVariable.IsRequired)), + resource.TestCheckResourceAttr(accessor, "regex", "regex"), ) t.Run("ScopeGlobal", func(t *testing.T) { diff --git a/env0/resource_configuration_variable.go b/env0/resource_configuration_variable.go index e90f9dca..2c797219 100644 --- a/env0/resource_configuration_variable.go +++ b/env0/resource_configuration_variable.go @@ -105,6 +105,11 @@ func resourceConfigurationVariable() *schema.Resource { Default: false, ConflictsWith: []string{"environment_id"}, }, + "regex": { + Type: schema.TypeString, + Description: "the value of this variable must match provided regular expression (enforced only in env0 UI)", + Optional: true, + }, }, } } @@ -148,6 +153,7 @@ func resourceConfigurationVariableCreate(ctx context.Context, d *schema.Resource format := client.Format(d.Get("format").(string)) isReadOnly := d.Get("is_read_only").(bool) isRequired := d.Get("is_required").(bool) + regex := d.Get("regex").(string) if err := validateNilValue(isReadOnly, isRequired, value); err != nil { return diag.Errorf(err.Error()) @@ -179,6 +185,7 @@ func resourceConfigurationVariableCreate(ctx context.Context, d *schema.Resource Format: format, IsReadonly: isReadOnly, IsRequired: isRequired, + Regex: regex, }) if err != nil { return diag.Errorf("could not create configurationVariable: %v", err) @@ -227,6 +234,7 @@ func resourceConfigurationVariableRead(ctx context.Context, d *schema.ResourceDa d.Set("is_sensitive", variable.IsSensitive) d.Set("is_read_only", variable.IsReadonly) d.Set("is_required", variable.IsRequired) + d.Set("regex", variable.Regex) if variable.Type != nil && *variable.Type == client.ConfigurationVariableTypeTerraform { d.Set("type", "terraform") } else { @@ -258,6 +266,7 @@ func resourceConfigurationVariableUpdate(ctx context.Context, d *schema.Resource format := client.Format(d.Get("format").(string)) isReadOnly := d.Get("is_read_only").(bool) isRequired := d.Get("is_required").(bool) + regex := d.Get("regex").(string) if err := validateNilValue(isReadOnly, isRequired, value); err != nil { return diag.Errorf(err.Error()) @@ -288,6 +297,7 @@ func resourceConfigurationVariableUpdate(ctx context.Context, d *schema.Resource Format: format, IsReadonly: isReadOnly, IsRequired: isRequired, + Regex: regex, }}) if err != nil { return diag.Errorf("could not update configurationVariable: %v", err) diff --git a/env0/resource_configuration_variable_test.go b/env0/resource_configuration_variable_test.go index ea049de7..09335e45 100644 --- a/env0/resource_configuration_variable_test.go +++ b/env0/resource_configuration_variable_test.go @@ -175,6 +175,61 @@ resource "{{.resourceType}}" "{{.projResourceName}}" { }) }) + t.Run("Create and update with regex", func(t *testing.T) { + initialVar := client.ConfigurationVariable{ + Id: "regex-var-id", + Name: "regex-var-name", + Regex: "initial-regex", + } + initialResource := resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": initialVar.Name, + "regex": initialVar.Regex, + }) + createParams := client.ConfigurationVariableCreateParams{ + Name: initialVar.Name, + Regex: initialVar.Regex, + Scope: client.ScopeGlobal, + } + + updatedVar := initialVar + updatedVar.Regex = "updated-regex" + updatedResource := resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": updatedVar.Name, + "regex": updatedVar.Regex, + }) + updateParams := createParams + updateParams.Regex = updatedVar.Regex + + steps := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: initialResource, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "name", initialVar.Name), + resource.TestCheckResourceAttr(accessor, "regex", initialVar.Regex), + ), + }, + { + Config: updatedResource, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "name", updatedVar.Name), + resource.TestCheckResourceAttr(accessor, "regex", updatedVar.Regex), + ), + }, + }, + } + + runUnitTest(t, steps, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().ConfigurationVariableCreate(createParams).Times(1).Return(initialVar, nil), + mock.EXPECT().ConfigurationVariablesById(initialVar.Id).Times(2).Return(initialVar, nil), + mock.EXPECT().ConfigurationVariableUpdate(client.ConfigurationVariableUpdateParams{CommonParams: updateParams, Id: updatedVar.Id}).Times(1).Return(updatedVar, nil), + mock.EXPECT().ConfigurationVariablesById(initialVar.Id).Times(1).Return(updatedVar, nil), + mock.EXPECT().ConfigurationVariableDelete(initialVar.Id).Times(1).Return(nil), + ) + }) + }) + t.Run("Create Enum", func(t *testing.T) { schema := client.ConfigurationVariableSchema{ Type: "string", diff --git a/tests/integration/003_configuration_variable/expected_outputs.json b/tests/integration/003_configuration_variable/expected_outputs.json index 31d53a54..3655e3c1 100644 --- a/tests/integration/003_configuration_variable/expected_outputs.json +++ b/tests/integration/003_configuration_variable/expected_outputs.json @@ -1,5 +1,6 @@ { "tested1_value": "fake value 1 after update", "tested3_enum_1": "First", - "tested3_enum_2": "Second" + "tested3_enum_2": "Second", + "regex" : "^test-\\d+$" } diff --git a/tests/integration/003_configuration_variable/main.tf b/tests/integration/003_configuration_variable/main.tf index 04691570..fd7a83f3 100644 --- a/tests/integration/003_configuration_variable/main.tf +++ b/tests/integration/003_configuration_variable/main.tf @@ -58,3 +58,19 @@ output "tested3_enum_2" { value = data.env0_configuration_variable.tested3.enum[1] sensitive = true } + +resource "env0_configuration_variable" "regex_var" { + project_id = env0_project.test_project.id + name = "regex_var" + regex = "^test-\\d+$" +} + +data "env0_configuration_variable" "regex_var" { + project_id = env0_project.test_project.id + name = "regex_var" + depends_on = [env0_configuration_variable.regex_var] +} + +output "regex" { + value = data.env0_configuration_variable.regex_var.regex +}