From 6a2af26182873163b7cdd175a3291fdd0579e440 Mon Sep 17 00:00:00 2001 From: Tomer Heber Date: Mon, 20 Feb 2023 06:47:39 -0600 Subject: [PATCH] Feat: custom project and organization TTL policy (#588) * Feat: custom project and organization TTL policy * removed rarity of notification * made some refactoring --- env0/resource_organization_policy.go | 45 +++++++++---------- env0/resource_organization_policy_test.go | 8 ++-- env0/resource_project_policy.go | 19 +++----- env0/resource_project_policy_test.go | 2 +- env0/utils.go | 36 +++++++++++++++ env0/utils_test.go | 50 +++++++++++++++++++++ env0/validators.go | 11 +++++ tests/integration/001_organization/main.tf | 4 +- tests/integration/011_policy/main.tf | 6 +-- tests/integration/025_notifications/main.tf | 9 ++-- 10 files changed, 138 insertions(+), 52 deletions(-) diff --git a/env0/resource_organization_policy.go b/env0/resource_organization_policy.go index 10ab5a85..d9ca3d4b 100644 --- a/env0/resource_organization_policy.go +++ b/env0/resource_organization_policy.go @@ -3,20 +3,13 @@ package env0 import ( "context" "fmt" - "strings" "github.com/env0/terraform-provider-env0/client" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -// The order is important (should be from shortest to longest). -// See the usage of getTtlIndex for context. -var allowedTtlValues = []string{"6-h", "12-h", "1-d", "3-d", "1-w", "2-w", "1-M"} - func resourceOrganizationPolicy() *schema.Resource { - allowedTtlValuesStr := fmt.Sprintf("(allowed values: %s)", strings.Join(allowedTtlValues, ", ")) - return &schema.Resource{ CreateContext: resourceOrganizationPolicyCreateOrUpdate, ReadContext: resourceOrganizationPolicyRead, @@ -26,15 +19,15 @@ func resourceOrganizationPolicy() *schema.Resource { Schema: map[string]*schema.Schema{ "max_ttl": { Type: schema.TypeString, - Description: "the maximum environment time-to-live allowed on deploy time " + allowedTtlValuesStr + ". omit for infinite ttl. must be equal or longer than default_ttl", + Description: "the maximum environment time-to-live allowed on deploy time. Format is - (Examples: 12-h, 3-d, 1-w, 1-M). Omit for infinite ttl. must be equal or longer than default_ttl", Optional: true, - ValidateDiagFunc: NewStringInValidator(allowedTtlValues), + ValidateDiagFunc: ValidateTtl, }, "default_ttl": { Type: schema.TypeString, - Description: "the default environment time-to-live allowed on deploy time " + allowedTtlValuesStr + ". omit for infinite ttl. must be equal or shorter than max_ttl", + Description: "the default environment time-to-live allowed on deploy time. Format is - (Examples: 12-h, 3-d, 1-w, 1-M). Omit for infinite ttl. must be equal or shorter than max_ttl", Optional: true, - ValidateDiagFunc: NewStringInValidator(allowedTtlValues), + ValidateDiagFunc: ValidateTtl, }, "do_not_report_skipped_status_checks": { Type: schema.TypeBool, @@ -71,16 +64,23 @@ func resourceOrganizationPolicyRead(ctx context.Context, d *schema.ResourceData, return nil } -func getTtlIndex(value *string) int { - if value != nil { - for i, v := range allowedTtlValues { - if *value == v { - return i - } - } +// Validate that default ttl is "less than or equal" max ttl. +func validateTtl(defaultTtl *string, maxTtl *string) error { + defaultDuration, err := ttlToDuration(defaultTtl) + if err != nil { + return fmt.Errorf("invalid default ttl: %v", err) } - return len(allowedTtlValues) + maxDuration, err := ttlToDuration(maxTtl) + if err != nil { + return fmt.Errorf("invalid max ttl: %v", err) + } + + if maxDuration < defaultDuration { + return fmt.Errorf("default ttl must not be larger than max ttl: %d %d", defaultTtl, maxTtl) + } + + return nil } func resourceOrganizationPolicyCreateOrUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { @@ -91,11 +91,8 @@ func resourceOrganizationPolicyCreateOrUpdate(ctx context.Context, d *schema.Res return diag.Errorf("schema resource data deserialization failed: %v", err) } - // Validate that default ttl is "less than or equal" max ttl. - defaultTtlIndex := getTtlIndex(payload.DefaultTtl) - maxTtlIndex := getTtlIndex(payload.MaxTtl) - if maxTtlIndex < defaultTtlIndex { - return diag.Errorf("default ttl must not be larger than max ttl") + if err := validateTtl(payload.DefaultTtl, payload.MaxTtl); err != nil { + return diag.FromErr(err) } organization, err := apiClient.OrganizationPolicyUpdate((payload)) diff --git a/env0/resource_organization_policy_test.go b/env0/resource_organization_policy_test.go index a487a606..298c846c 100644 --- a/env0/resource_organization_policy_test.go +++ b/env0/resource_organization_policy_test.go @@ -25,8 +25,8 @@ func TestUnitOrganizationPolicyResource(t *testing.T) { organization := client.Organization{ Id: organizationId, Name: "name", - MaxTtl: stringPtr("3-d"), - DefaultTtl: stringPtr("12-h"), + MaxTtl: stringPtr("4-d"), + DefaultTtl: stringPtr("13-h"), DoNotReportSkippedStatusChecks: false, DoNotConsiderMergeCommitsForPrPlans: true, EnableOidc: false, @@ -35,7 +35,7 @@ func TestUnitOrganizationPolicyResource(t *testing.T) { organizationUpdated := client.Organization{ Id: organizationId, Name: "name", - DefaultTtl: stringPtr("1-M"), + DefaultTtl: stringPtr("2-M"), DoNotReportSkippedStatusChecks: true, DoNotConsiderMergeCommitsForPrPlans: false, EnableOidc: true, @@ -104,7 +104,7 @@ func TestUnitOrganizationPolicyResource(t *testing.T) { Steps: []resource.TestStep{ { Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ - "max_ttl": "12-h", + "max_ttl": "23-h", "default_ttl": "1-d", }), ExpectError: regexp.MustCompile("default ttl must not be larger than max ttl"), diff --git a/env0/resource_project_policy.go b/env0/resource_project_policy.go index 5cb3c352..d38eab5a 100644 --- a/env0/resource_project_policy.go +++ b/env0/resource_project_policy.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "strings" "github.com/env0/terraform-provider-env0/client" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" @@ -12,9 +11,6 @@ import ( ) func resourcePolicy() *schema.Resource { - allowedProjectTtlValues := append(allowedTtlValues, "Infinite", "inherit") - allowedProjectTtlValuesStr := fmt.Sprintf("(allowed values: %s)", strings.Join(allowedProjectTtlValues, ", ")) - return &schema.Resource{ CreateContext: resourcePolicyCreate, ReadContext: resourcePolicyRead, @@ -91,17 +87,17 @@ func resourcePolicy() *schema.Resource { }, "max_ttl": { Type: schema.TypeString, - Description: "the maximum environment time-to-live allowed on deploy time " + allowedProjectTtlValuesStr + " Default value is 'inherit' which inherits the organization policy. must be equal or longer than default_ttl", + Description: "the maximum environment time-to-live allowed on deploy time. Format is - (Examples: 12-h, 3-d, 1-w, 1-M). Default value is 'inherit' which inherits the organization policy. must be equal or longer than default_ttl", Optional: true, Default: "inherit", - ValidateDiagFunc: NewStringInValidator(allowedProjectTtlValues), + ValidateDiagFunc: ValidateTtl, }, "default_ttl": { Type: schema.TypeString, - Description: "the default environment time-to-live allowed on deploy time " + allowedProjectTtlValuesStr + ". Default value is 'inherit' which inherits the organization policy. must be equal or shorter than max_ttl", + Description: "the default environment time-to-live allowed on deploy time. Format is - (Examples: 12-h, 3-d, 1-w, 1-M). Default value is 'inherit' which inherits the organization policy. must be equal or shorter than max_ttl", Optional: true, Default: "inherit", - ValidateDiagFunc: NewStringInValidator(allowedProjectTtlValues), + ValidateDiagFunc: ValidateTtl, }, }, } @@ -151,11 +147,8 @@ func resourcePolicyUpdate(ctx context.Context, d *schema.ResourceData, meta inte return diag.Errorf("max_ttl and default_ttl must both inherit organization settings or override them") } - // Validate that default ttl is "less than or equal" max ttl. - defaultTtlIndex := getTtlIndex(&payload.DefaultTtl) - maxTtlIndex := getTtlIndex(&payload.MaxTtl) - if maxTtlIndex < defaultTtlIndex { - return diag.Errorf("default ttl must not be larger than max ttl") + if err := validateTtl(&payload.DefaultTtl, &payload.MaxTtl); err != nil { + return diag.FromErr(err) } if payload.DefaultTtl == "Infinite" { diff --git a/env0/resource_project_policy_test.go b/env0/resource_project_policy_test.go index f4745ead..526a19ce 100644 --- a/env0/resource_project_policy_test.go +++ b/env0/resource_project_policy_test.go @@ -41,7 +41,7 @@ func TestUnitPolicyResource(t *testing.T) { SkipRedundantDeployments: false, UpdatedBy: "updater0", MaxTtl: nil, - DefaultTtl: stringPtr("6-h"), + DefaultTtl: stringPtr("7-h"), } resetPolicy := client.Policy{ diff --git a/env0/utils.go b/env0/utils.go index 9f74b7b9..d36a840a 100644 --- a/env0/utils.go +++ b/env0/utils.go @@ -3,15 +3,19 @@ package env0 import ( "errors" "fmt" + "math" "reflect" "regexp" + "strconv" "strings" + "time" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") +var matchTtl = regexp.MustCompile("^([1-9][0-9]*)-([M|w|d|h])$") type CustomResourceDataField interface { ReadResourceData(fieldName string, d *schema.ResourceData) error @@ -453,3 +457,35 @@ func writeResourceDataEx(prefix string, i interface{}, d *schema.ResourceData) e } return writeResourceDataSlice([]interface{}{i}, prefix, d) } + +func ttlToDuration(ttl *string) (time.Duration, error) { + if ttl == nil || *ttl == "" || *ttl == "Infinite" || *ttl == "inherit" { + return math.MaxInt64, nil + } + + match := matchTtl.FindStringSubmatch(*ttl) + if match == nil { + return 0, fmt.Errorf("invalid TTL format %s", *ttl) + } + + numberStr := match[1] + number, err := strconv.Atoi(numberStr) + if err != nil { + return 0, fmt.Errorf("invalid TTL format %s - not a number: %w", *ttl, err) + } + // M/w/d/h + + var hours int = number + + switch rangeType := match[2]; rangeType { + case "M": + // 'M' varies each month. Assuming it's 30 days. + hours *= 30 * 24 + case "w": + hours *= 7 * 24 + case "d": + hours *= 24 + } + + return time.ParseDuration(fmt.Sprintf("%dh", hours)) +} diff --git a/env0/utils_test.go b/env0/utils_test.go index f7e85ec1..4c1d0569 100644 --- a/env0/utils_test.go +++ b/env0/utils_test.go @@ -1,7 +1,9 @@ package env0 import ( + "math" "testing" + "time" "github.com/env0/terraform-provider-env0/client" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -411,3 +413,51 @@ func TestReadSubEnvironment(t *testing.T) { require.Len(t, subEnvironments, 2) require.Equal(t, expectedSubEnvironments, subEnvironments) } + +func TestTTLToDuration(t *testing.T) { + t.Run("hours", func(t *testing.T) { + duration, err := ttlToDuration(stringPtr("2-h")) + require.Nil(t, err) + require.Equal(t, time.Duration(3600*2*1000000000), duration) + }) + + t.Run("days", func(t *testing.T) { + duration, err := ttlToDuration(stringPtr("1-d")) + require.Nil(t, err) + require.Equal(t, time.Duration(3600*24*1000000000), duration) + }) + + t.Run("weeks", func(t *testing.T) { + duration, err := ttlToDuration(stringPtr("3-w")) + require.Nil(t, err) + require.Equal(t, time.Duration(21*3600*24*1000000000), duration) + }) + + t.Run("months", func(t *testing.T) { + duration, err := ttlToDuration(stringPtr("1-M")) + require.Nil(t, err) + require.Equal(t, time.Duration(30*3600*24*1000000000), duration) + }) + + t.Run("inherit", func(t *testing.T) { + duration, err := ttlToDuration(stringPtr("inherit")) + require.Nil(t, err) + require.Equal(t, time.Duration(math.MaxInt64), duration) + }) + + t.Run("Infinite", func(t *testing.T) { + duration, err := ttlToDuration(stringPtr("Infinite")) + require.Nil(t, err) + require.Equal(t, time.Duration(math.MaxInt64), duration) + }) + + t.Run("invalid format", func(t *testing.T) { + _, err := ttlToDuration(stringPtr("2-F")) + require.Error(t, err) + }) + + t.Run("invalid format - not a number", func(t *testing.T) { + _, err := ttlToDuration(stringPtr("f-M")) + require.Error(t, err) + }) +} diff --git a/env0/validators.go b/env0/validators.go index 47a4ec8b..3eb05f50 100644 --- a/env0/validators.go +++ b/env0/validators.go @@ -101,3 +101,14 @@ func NewGreaterThanValidator(greaterThan int) schema.SchemaValidateDiagFunc { return nil } } + +func ValidateTtl(i interface{}, path cty.Path) diag.Diagnostics { + ttl := i.(string) + + _, err := ttlToDuration(&ttl) + if err != nil { + return diag.FromErr(err) + } + + return nil +} diff --git a/tests/integration/001_organization/main.tf b/tests/integration/001_organization/main.tf index 74a8b8ea..46cf7326 100644 --- a/tests/integration/001_organization/main.tf +++ b/tests/integration/001_organization/main.tf @@ -5,8 +5,8 @@ output "organization_name" { } resource "env0_organization_policy" "my_organization_policy" { - max_ttl = "1-M" - default_ttl = var.second_run ? "6-h" : "12-h" + max_ttl = "2-M" + default_ttl = var.second_run ? "7-h" : "13-h" do_not_consider_merge_commits_for_pr_plans = var.second_run ? false : true enable_oidc = var.second_run ? false : true } diff --git a/tests/integration/011_policy/main.tf b/tests/integration/011_policy/main.tf index 7b02f7ea..a5e1d730 100644 --- a/tests/integration/011_policy/main.tf +++ b/tests/integration/011_policy/main.tf @@ -39,8 +39,8 @@ resource "env0_project_policy" "test_policy_ttl" { skip_apply_when_plan_is_empty = true disable_destroy_environments = true skip_redundant_deployments = true - max_ttl = "3-d" - default_ttl = "12-h" + max_ttl = "4-d" + default_ttl = "14-h" } resource "env0_project_policy" "test_policy_infinite" { @@ -53,5 +53,5 @@ resource "env0_project_policy" "test_policy_infinite" { disable_destroy_environments = true skip_redundant_deployments = true max_ttl = "Infinite" - default_ttl = var.second_run ? "3-d" : "Infinite" + default_ttl = var.second_run ? "4-d" : "Infinite" } diff --git a/tests/integration/025_notifications/main.tf b/tests/integration/025_notifications/main.tf index a04059ad..5c8186b1 100644 --- a/tests/integration/025_notifications/main.tf +++ b/tests/integration/025_notifications/main.tf @@ -1,20 +1,19 @@ provider "random" {} resource "random_string" "random" { - length = 8 + length = 20 special = false - min_lower = 8 + min_lower = 20 } - resource "env0_notification" "test_notification_1" { - name = "notification-${random_string.random.result}-1" + name = "notification123-${random_string.random.result}-1" type = "Slack" value = "https://someurl1.com" } resource "env0_notification" "test_notification_2" { - name = "notification-${random_string.random.result}-2" + name = "notification123-${random_string.random.result}-2" type = "Teams" value = "https://someurl2.com" }