From 568cbb32c169e962715547d8a0b46bdc6cadeb14 Mon Sep 17 00:00:00 2001 From: Bojan Zelic Date: Mon, 10 May 2021 16:25:17 -0700 Subject: [PATCH 1/5] service token - add support for renewals --- ...source_cloudflare_access_service_tokens.go | 39 +++++++++++++++++++ ...e_cloudflare_access_service_tokens_test.go | 4 ++ 2 files changed, 43 insertions(+) diff --git a/cloudflare/resource_cloudflare_access_service_tokens.go b/cloudflare/resource_cloudflare_access_service_tokens.go index 515f34f03d..9f81681a66 100644 --- a/cloudflare/resource_cloudflare_access_service_tokens.go +++ b/cloudflare/resource_cloudflare_access_service_tokens.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "time" "github.com/cloudflare/cloudflare-go" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" @@ -19,6 +20,8 @@ func resourceCloudflareAccessServiceToken() *schema.Resource { State: resourceCloudflareAccessServiceTokenImport, }, + CustomizeDiff: resourceCloudflareAccessServiceTokenCustomizeDiff, + Schema: map[string]*schema.Schema{ "account_id": { Type: schema.TypeString, @@ -43,10 +46,44 @@ func resourceCloudflareAccessServiceToken() *schema.Resource { Computed: true, Sensitive: true, }, + "expires_at": { + Type: schema.TypeString, + Computed: true, + }, + "min_days_for_renewal": { + Type: schema.TypeInt, + Optional: true, + Default: 0, + }, }, } } +func resourceCloudflareAccessServiceTokenCustomizeDiff(d *schema.ResourceDiff, meta interface{}) error { + + mindays := d.Get("min_days_for_renewal").(int) + if mindays > 0 { + expires_at := d.Get("expires_at").(string) + + if expires_at != "" { + expected_expiration_date, err := time.Parse(time.RFC3339, expires_at) + + if err != nil { + return err + } + + expiration_date := time.Now().Add(time.Duration(mindays) * 24 * time.Hour) + + if expiration_date.After(expected_expiration_date) { + d.SetNewComputed("expires_at") + d.SetNewComputed("client_secret") + } + } + } + + return nil +} + func resourceCloudflareAccessServiceTokenRead(d *schema.ResourceData, meta interface{}) error { client := meta.(*cloudflare.API) @@ -71,6 +108,7 @@ func resourceCloudflareAccessServiceTokenRead(d *schema.ResourceData, meta inter if token.ID == d.Id() { d.Set("name", token.Name) d.Set("client_id", token.ClientID) + d.Set("expires_at", token.ExpiresAt.Format(time.RFC3339)) } } @@ -100,6 +138,7 @@ func resourceCloudflareAccessServiceTokenCreate(d *schema.ResourceData, meta int d.Set("name", serviceToken.Name) d.Set("client_id", serviceToken.ClientID) d.Set("client_secret", serviceToken.ClientSecret) + d.Set("expires_at", serviceToken.ExpiresAt.Format(time.RFC3339)) resourceCloudflareAccessServiceTokenRead(d, meta) diff --git a/cloudflare/resource_cloudflare_access_service_tokens_test.go b/cloudflare/resource_cloudflare_access_service_tokens_test.go index ce4175408f..f9bb9eca7d 100644 --- a/cloudflare/resource_cloudflare_access_service_tokens_test.go +++ b/cloudflare/resource_cloudflare_access_service_tokens_test.go @@ -38,6 +38,7 @@ func TestAccAccessServiceTokenCreate(t *testing.T) { resource.TestCheckResourceAttr(name, "name", resourceName), resource.TestCheckResourceAttrSet(name, "client_id"), resource.TestCheckResourceAttrSet(name, "client_secret"), + resource.TestCheckResourceAttrSet(name, "expires_at"), ), }, }, @@ -54,6 +55,7 @@ func TestAccAccessServiceTokenCreate(t *testing.T) { resource.TestCheckResourceAttr(name, "name", resourceName), resource.TestCheckResourceAttrSet(name, "client_id"), resource.TestCheckResourceAttrSet(name, "client_secret"), + resource.TestCheckResourceAttrSet(name, "expires_at"), ), }, }, @@ -147,6 +149,7 @@ func TestAccAccessServiceTokenDelete(t *testing.T) { resource.TestCheckResourceAttr(name, "name", resourceName), resource.TestCheckResourceAttrSet(name, "client_id"), resource.TestCheckResourceAttrSet(name, "client_secret"), + resource.TestCheckResourceAttrSet(name, "expires_at"), ), }, }, @@ -164,6 +167,7 @@ func TestAccAccessServiceTokenDelete(t *testing.T) { resource.TestCheckResourceAttr(name, "name", resourceName), resource.TestCheckResourceAttrSet(name, "client_id"), resource.TestCheckResourceAttrSet(name, "client_secret"), + resource.TestCheckResourceAttrSet(name, "expires_at"), ), }, }, From 68aeff849520c501b329e9c381d42eb8109c1a02 Mon Sep 17 00:00:00 2001 From: Bojan Zelic Date: Mon, 10 May 2021 16:26:08 -0700 Subject: [PATCH 2/5] service_token renewal add documentation --- .../docs/r/access_service_token.html.markdown | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/website/docs/r/access_service_token.html.markdown b/website/docs/r/access_service_token.html.markdown index 2f43af4271..1ec7ec066a 100644 --- a/website/docs/r/access_service_token.html.markdown +++ b/website/docs/r/access_service_token.html.markdown @@ -18,6 +18,21 @@ resource "cloudflare_access_service_token" "my_app" { account_id = "d41d8cd98f00b204e9800998ecf8427e" name = "CI/CD app" } + +# Generate a service token that will renew if terraform is ran within 30 days of expiration +resource "cloudflare_access_service_token" "my_app" { + account_id = "d41d8cd98f00b204e9800998ecf8427e" + name = "CI/CD app renewed" + + min_days_for_renewal = 30 + + # This flag is important to set if min_days_for_renewal is defined otherwise + # there will be a brief period where the service relying on that token + # will not have access due to the resource being deleted + lifecycle { + create_before_destroy = true + } +} ``` ## Argument Reference @@ -29,6 +44,7 @@ The following arguments are supported: * `account_id` - (Optional) The ID of the account where the Access Service is being created. Conflicts with `zone_id`. * `zone_id` - (Optional) The ID of the zone where the Access Service is being created. Conflicts with `account_id`. * `name` - (Required) Friendly name of the token's intent. +* `min_days_for_renewal` - (Optional) Regenerates the token if terraform is run within the specified amount of days before expiration ## Attributes Reference @@ -36,6 +52,7 @@ The following attributes are exported: * `client_id` - UUID client ID associated with the Service Token. * `client_secret` - A secret for interacting with Access protocols. +* `expires_at` - Date when the token expires ## Import From 29eb7b8aaeadac8a992104441b51fd9256fd32d3 Mon Sep 17 00:00:00 2001 From: Bojan Zelic Date: Thu, 13 May 2021 09:40:20 -0700 Subject: [PATCH 3/5] refactor access service token customizediff function to use helper --- .../resource_cloudflare_access_service_tokens.go | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/cloudflare/resource_cloudflare_access_service_tokens.go b/cloudflare/resource_cloudflare_access_service_tokens.go index 9f81681a66..bad7771a59 100644 --- a/cloudflare/resource_cloudflare_access_service_tokens.go +++ b/cloudflare/resource_cloudflare_access_service_tokens.go @@ -7,6 +7,7 @@ import ( "time" "github.com/cloudflare/cloudflare-go" + "github.com/hashicorp/terraform-plugin-sdk/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" ) @@ -20,7 +21,7 @@ func resourceCloudflareAccessServiceToken() *schema.Resource { State: resourceCloudflareAccessServiceTokenImport, }, - CustomizeDiff: resourceCloudflareAccessServiceTokenCustomizeDiff, + CustomizeDiff: customdiff.ComputedIf("expires_at", resourceCloudflareAccessServiceTokenExpireDiff), Schema: map[string]*schema.Schema{ "account_id": { @@ -59,29 +60,24 @@ func resourceCloudflareAccessServiceToken() *schema.Resource { } } -func resourceCloudflareAccessServiceTokenCustomizeDiff(d *schema.ResourceDiff, meta interface{}) error { - +func resourceCloudflareAccessServiceTokenExpireDiff(d *schema.ResourceDiff, m interface{}) bool { mindays := d.Get("min_days_for_renewal").(int) if mindays > 0 { expires_at := d.Get("expires_at").(string) if expires_at != "" { - expected_expiration_date, err := time.Parse(time.RFC3339, expires_at) - - if err != nil { - return err - } + expected_expiration_date, _ := time.Parse(time.RFC3339, expires_at) expiration_date := time.Now().Add(time.Duration(mindays) * 24 * time.Hour) if expiration_date.After(expected_expiration_date) { - d.SetNewComputed("expires_at") d.SetNewComputed("client_secret") + return true } } } - return nil + return false } func resourceCloudflareAccessServiceTokenRead(d *schema.ResourceData, meta interface{}) error { From c9a99765d6c10fe16ef41eb16adb6297be2d4c95 Mon Sep 17 00:00:00 2001 From: Bojan Zelic Date: Thu, 13 May 2021 14:51:28 -0700 Subject: [PATCH 4/5] force new resource if client_id, client_secret, or expires_at changes --- cloudflare/resource_cloudflare_access_service_tokens.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cloudflare/resource_cloudflare_access_service_tokens.go b/cloudflare/resource_cloudflare_access_service_tokens.go index bad7771a59..adb779aa68 100644 --- a/cloudflare/resource_cloudflare_access_service_tokens.go +++ b/cloudflare/resource_cloudflare_access_service_tokens.go @@ -41,15 +41,18 @@ func resourceCloudflareAccessServiceToken() *schema.Resource { "client_id": { Type: schema.TypeString, Computed: true, + ForceNew: true, }, "client_secret": { Type: schema.TypeString, Computed: true, Sensitive: true, + ForceNew: true, }, "expires_at": { Type: schema.TypeString, Computed: true, + ForceNew: true, }, "min_days_for_renewal": { Type: schema.TypeInt, From cdaca76f73f1ded6b1fe0ccc8648255cacaad322 Mon Sep 17 00:00:00 2001 From: Bojan Zelic Date: Thu, 13 May 2021 14:52:10 -0700 Subject: [PATCH 5/5] add acceptance test for service token expiration renewal --- ...e_cloudflare_access_service_tokens_test.go | 103 ++++++++++++++++-- 1 file changed, 93 insertions(+), 10 deletions(-) diff --git a/cloudflare/resource_cloudflare_access_service_tokens_test.go b/cloudflare/resource_cloudflare_access_service_tokens_test.go index f9bb9eca7d..98ee298cc0 100644 --- a/cloudflare/resource_cloudflare_access_service_tokens_test.go +++ b/cloudflare/resource_cloudflare_access_service_tokens_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "strconv" "strings" "testing" @@ -32,7 +33,7 @@ func TestAccAccessServiceTokenCreate(t *testing.T) { Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testCloudflareAccessServiceTokenBasicConfig(resourceName, resourceName, AccessIdentifier{Type: AccountType, Value: accountID}), + Config: testCloudflareAccessServiceTokenBasicConfig(resourceName, resourceName, AccessIdentifier{Type: AccountType, Value: accountID}, 0), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(name, "account_id", accountID), resource.TestCheckResourceAttr(name, "name", resourceName), @@ -49,7 +50,7 @@ func TestAccAccessServiceTokenCreate(t *testing.T) { Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testCloudflareAccessServiceTokenBasicConfig(resourceName, resourceName, AccessIdentifier{Type: ZoneType, Value: zoneID}), + Config: testCloudflareAccessServiceTokenBasicConfig(resourceName, resourceName, AccessIdentifier{Type: ZoneType, Value: zoneID}, 0), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(name, "zone_id", zoneID), resource.TestCheckResourceAttr(name, "name", resourceName), @@ -85,13 +86,13 @@ func TestAccAccessServiceTokenUpdate(t *testing.T) { Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testCloudflareAccessServiceTokenBasicConfig(resourceName, resourceName, AccessIdentifier{Type: AccountType, Value: accountID}), + Config: testCloudflareAccessServiceTokenBasicConfig(resourceName, resourceName, AccessIdentifier{Type: AccountType, Value: accountID}, 0), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(name, "name", resourceName), ), }, { - Config: testCloudflareAccessServiceTokenBasicConfig(resourceName, resourceName+"-updated", AccessIdentifier{Type: AccountType, Value: accountID}), + Config: testCloudflareAccessServiceTokenBasicConfig(resourceName, resourceName+"-updated", AccessIdentifier{Type: AccountType, Value: accountID}, 0), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(name, "name", resourceName+"-updated"), ), @@ -104,13 +105,13 @@ func TestAccAccessServiceTokenUpdate(t *testing.T) { Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: testCloudflareAccessServiceTokenBasicConfig(resourceName, resourceName, AccessIdentifier{Type: ZoneType, Value: zoneID}), + Config: testCloudflareAccessServiceTokenBasicConfig(resourceName, resourceName, AccessIdentifier{Type: ZoneType, Value: zoneID}, 0), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(name, "name", resourceName), ), }, { - Config: testCloudflareAccessServiceTokenBasicConfig(resourceName, resourceName+"-updated", AccessIdentifier{Type: ZoneType, Value: zoneID}), + Config: testCloudflareAccessServiceTokenBasicConfig(resourceName, resourceName+"-updated", AccessIdentifier{Type: ZoneType, Value: zoneID}, 0), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(name, "name", resourceName+"-updated"), ), @@ -119,6 +120,87 @@ func TestAccAccessServiceTokenUpdate(t *testing.T) { }) } +func TestAccAccessServiceTokenUpdateWithExpiration(t *testing.T) { + // Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the Access + // Service Tokens endpoint does not yet support the API tokens and it + // results in misleading state error messages. + if os.Getenv("CLOUDFLARE_API_TOKEN") != "" { + defer func(apiToken string) { + os.Setenv("CLOUDFLARE_API_TOKEN", apiToken) + }(os.Getenv("CLOUDFLARE_API_TOKEN")) + os.Setenv("CLOUDFLARE_API_TOKEN", "") + } + + rnd := generateRandomResourceName() + var initialState terraform.ResourceState + + name := fmt.Sprintf("cloudflare_access_service_token.tf-acc-%s", rnd) + resourceName := strings.Split(name, ".")[1] + expirationTime := 365 + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccessAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testCloudflareAccessServiceTokenBasicConfig(resourceName, resourceName, AccessIdentifier{Type: ZoneType, Value: zoneID}, expirationTime), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudflareAccessServiceTokenSaved(name, &initialState), + resource.TestCheckResourceAttr(name, "min_days_for_renewal", strconv.Itoa(expirationTime)), + ), + //Expiration of 365 will always force a new resource as long as the tokens expire in 365 days in cloudflare + ExpectNonEmptyPlan: true, + }, + { + Config: testCloudflareAccessServiceTokenBasicConfig(resourceName, resourceName, AccessIdentifier{Type: ZoneType, Value: zoneID}, expirationTime), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(name, "min_days_for_renewal", strconv.Itoa(expirationTime)), + testAccCheckCloudflareAccessServiceTokenRenewed(name, &initialState), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckCloudflareAccessServiceTokenSaved(n string, resourceState *terraform.ResourceState) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Access Token ID is set") + } + + *resourceState = *rs + + return nil + } +} + +func testAccCheckCloudflareAccessServiceTokenRenewed(n string, oldResourceState *terraform.ResourceState) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Access Token ID is set") + } + + for _, attribute := range []string{"expires_at", "client_secret"} { + if rs.Primary.Attributes[attribute] == oldResourceState.Primary.Attributes[attribute] { + return fmt.Errorf("Resource attribute '%s' has not changed. Expected change between old state and new", attribute) + } + } + + return nil + } +} + func TestAccAccessServiceTokenDelete(t *testing.T) { // Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the Access // Service Tokens endpoint does not yet support the API tokens and it @@ -143,7 +225,7 @@ func TestAccAccessServiceTokenDelete(t *testing.T) { CheckDestroy: testAccCheckCloudflareAccessServiceTokenDestroy, Steps: []resource.TestStep{ { - Config: testCloudflareAccessServiceTokenBasicConfig(resourceName, resourceName, AccessIdentifier{Type: AccountType, Value: accountID}), + Config: testCloudflareAccessServiceTokenBasicConfig(resourceName, resourceName, AccessIdentifier{Type: AccountType, Value: accountID}, 0), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(name, "account_id", accountID), resource.TestCheckResourceAttr(name, "name", resourceName), @@ -161,7 +243,7 @@ func TestAccAccessServiceTokenDelete(t *testing.T) { CheckDestroy: testAccCheckCloudflareAccessServiceTokenDestroy, Steps: []resource.TestStep{ { - Config: testCloudflareAccessServiceTokenBasicConfig(resourceName, resourceName, AccessIdentifier{Type: ZoneType, Value: zoneID}), + Config: testCloudflareAccessServiceTokenBasicConfig(resourceName, resourceName, AccessIdentifier{Type: ZoneType, Value: zoneID}, 0), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(name, "zone_id", zoneID), resource.TestCheckResourceAttr(name, "name", resourceName), @@ -174,12 +256,13 @@ func TestAccAccessServiceTokenDelete(t *testing.T) { }) } -func testCloudflareAccessServiceTokenBasicConfig(resourceName string, tokenName string, identifier AccessIdentifier) string { +func testCloudflareAccessServiceTokenBasicConfig(resourceName string, tokenName string, identifier AccessIdentifier, minDaysForRenewal int) string { return fmt.Sprintf(` resource "cloudflare_access_service_token" "%[1]s" { %[3]s_id = "%[4]s" name = "%[2]s" -}`, resourceName, tokenName, identifier.Type, identifier.Value) + min_days_for_renewal ="%[5]d" +}`, resourceName, tokenName, identifier.Type, identifier.Value, minDaysForRenewal) } func testAccCheckCloudflareAccessServiceTokenDestroy(s *terraform.State) error {