Skip to content

Commit

Permalink
Feat: custom project and organization TTL policy (#588)
Browse files Browse the repository at this point in the history
* Feat: custom project and organization TTL policy

* removed rarity of notification

* made some refactoring
  • Loading branch information
TomerHeber committed Feb 20, 2023
1 parent 07f75c9 commit 6a2af26
Show file tree
Hide file tree
Showing 10 changed files with 138 additions and 52 deletions.
45 changes: 21 additions & 24 deletions env0/resource_organization_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 <number>-<M/w/d/h> (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 <number>-<M/w/d/h> (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,
Expand Down Expand Up @@ -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 {
Expand All @@ -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))
Expand Down
8 changes: 4 additions & 4 deletions env0/resource_organization_policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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"),
Expand Down
19 changes: 6 additions & 13 deletions env0/resource_project_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,13 @@ import (
"context"
"errors"
"fmt"
"strings"

"github.com/env0/terraform-provider-env0/client"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func resourcePolicy() *schema.Resource {
allowedProjectTtlValues := append(allowedTtlValues, "Infinite", "inherit")
allowedProjectTtlValuesStr := fmt.Sprintf("(allowed values: %s)", strings.Join(allowedProjectTtlValues, ", "))

return &schema.Resource{
CreateContext: resourcePolicyCreate,
ReadContext: resourcePolicyRead,
Expand Down Expand Up @@ -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 <number>-<M/w/d/h> (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 <number>-<M/w/d/h> (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,
},
},
}
Expand Down Expand Up @@ -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" {
Expand Down
2 changes: 1 addition & 1 deletion env0/resource_project_policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
36 changes: 36 additions & 0 deletions env0/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
}
50 changes: 50 additions & 0 deletions env0/utils_test.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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)
})
}
11 changes: 11 additions & 0 deletions env0/validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
4 changes: 2 additions & 2 deletions tests/integration/001_organization/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
6 changes: 3 additions & 3 deletions tests/integration/011_policy/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand All @@ -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"
}
9 changes: 4 additions & 5 deletions tests/integration/025_notifications/main.tf
Original file line number Diff line number Diff line change
@@ -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"
}
Expand Down

0 comments on commit 6a2af26

Please sign in to comment.