Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 404-retries for pubsub and poll async utils #5813

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/3155.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
pubsub: Add polling to ensure correct resource state for negative-cached PubSub resources
```
111 changes: 26 additions & 85 deletions google/cloudrun_polling.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
package google

import (
"errors"
"fmt"
"log"

"time"

"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/errwrap"
)

const readyStatus string = "Ready"
const readyStatusType string = "Ready"
const pendingCertificateReason string = "CertificatePending"

type Condition struct {
Type string
Expand All @@ -31,91 +30,33 @@ type KnativeStatus struct {
}
}

// ConditionByType is a helper method for extracting a given condition
func (s KnativeStatus) ConditionByType(typ string) *Condition {
for _, condition := range s.Status.Conditions {
if condition.Type == typ {
c := condition
return &c
}
func PollCheckKnativeStatus(resp map[string]interface{}, respErr error) PollResult {
if respErr != nil {
return ErrorPollResult(respErr)
}
return nil
}

// LatestMessage will return a human consumable status of the resource. This can
// be used to determine the human actionable error the GET doesn't return an explicit
// error but the resource is in an error state.
func (s KnativeStatus) LatestMessage() string {
c := s.ConditionByType(readyStatus)
if c != nil {
return fmt.Sprintf("%s - %s", c.Reason, c.Message)
s := KnativeStatus{}
if err := Convert(resp, &s); err != nil {
return ErrorPollResult(errwrap.Wrapf("unable to get KnativeStatus: {{err}}", err))
}

return ""
}

// State will return a string representing the status of the Ready condition.
// No other conditions are currently returned as part of the state.
func (s KnativeStatus) State(res interface{}) string {
for _, condition := range s.Status.Conditions {
if condition.Type == "Ready" {
// DomainMapping can enter a 'terminal' state of waiting for external verification
// of DNS records.
if condition.Reason == "CertificatePending" {
return "Ready:CertificatePending"
}
return fmt.Sprintf("%s:%s", condition.Type, condition.Status)
}
}
return "Empty"
}

// CloudRunPolling allows for polling against a cloud run resource that implements the
// Kubernetes style status schema.
type CloudRunPolling struct {
Config *Config
WaitURL string
}

func (p *CloudRunPolling) PendingStates() []string {
return []string{"Ready:Unknown", "Empty"}
}
func (p *CloudRunPolling) TargetStates() []string {
return []string{"Ready:True", "Ready:CertificatePending"}
}
func (p *CloudRunPolling) ErrorStates() []string {
return []string{"Ready:False"}
}

func cloudRunPollingWaitTime(config *Config, res map[string]interface{}, project, url, activity string, timeoutMinutes int) error {
w := &CloudRunPolling{}

scc := &resource.StateChangeConf{
Pending: w.PendingStates(),
Target: w.TargetStates(),
Refresh: func() (interface{}, string, error) {
res, err := sendRequest(config, "GET", project, url, nil)
if err != nil {
return res, "", err
}

status := KnativeStatus{}
err = Convert(res, &status)
if err != nil {
return res, "", err
}

for _, errState := range w.ErrorStates() {
if status.State(res) == errState {
err = errors.New(status.LatestMessage())
if condition.Type == readyStatusType {
log.Printf("[DEBUG] checking KnativeStatus Ready condition %s: %s", condition.Status, condition.Message)
switch condition.Status {
case "True":
// Resource is ready
return SuccessPollResult()
case "Unknown":
// DomainMapping can enter a 'terminal' state where "Ready" status is "Unknown"
// but the resource is waiting for external verification of DNS records.
if condition.Reason == pendingCertificateReason {
return SuccessPollResult()
}
return PendingStatusPollResult(fmt.Sprintf("%s:%s", condition.Status, condition.Message))
case "False":
return ErrorPollResult(fmt.Errorf(`resource is in failed state "Ready:False", message: %s`, condition.Message))
}

return res, status.State(res), err
},
Timeout: time.Duration(timeoutMinutes) * time.Minute,
}
}

_, err := scc.WaitForState()
return err
return PendingStatusPollResult("no status yet")
}
55 changes: 55 additions & 0 deletions google/common_polling.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package google

import (
"fmt"
"log"
"time"

"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
)

type (
// Function handling for polling for a resource
PollReadFunc func() (resp map[string]interface{}, respErr error)

// Function to check the response from polling once
PollCheckResponseFunc func(resp map[string]interface{}, respErr error) PollResult

PollResult *resource.RetryError
)

// Helper functions to construct result of single pollRead as return result for a PollCheckResponseFunc
func ErrorPollResult(err error) PollResult {
return resource.NonRetryableError(err)
}

func PendingStatusPollResult(status string) PollResult {
return resource.RetryableError(fmt.Errorf("got pending status %q", status))
}

func SuccessPollResult() PollResult {
return nil
}

func PollingWaitTime(pollF PollReadFunc, checkResponse PollCheckResponseFunc, activity string, timeout time.Duration) error {
log.Printf("[DEBUG] %s: Polling until expected state is read", activity)
return resource.Retry(timeout, func() *resource.RetryError {
readResp, readErr := pollF()
return checkResponse(readResp, readErr)
})
}

/**
* Common PollCheckResponseFunc implementations
*/

// PollCheckForExistence waits for a successful response, continues polling on 404, and returns any other error.
func PollCheckForExistence(_ map[string]interface{}, respErr error) PollResult {
if respErr != nil {
if isGoogleApiErrorWithCode(respErr, 404) {
return PendingStatusPollResult("not found")
}
return ErrorPollResult(respErr)
}
return SuccessPollResult()
}
44 changes: 35 additions & 9 deletions google/resource_cloud_run_domain_mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
"google.golang.org/api/googleapi"
)

func resourceCloudRunDomainMapping() *schema.Resource {
Expand Down Expand Up @@ -292,15 +293,7 @@ func resourceCloudRunDomainMappingCreate(d *schema.ResourceData, meta interface{
}
d.SetId(id)

waitURL, err := replaceVars(d, config, "{{CloudRunBasePath}}apis/domains.cloudrun.com/v1/namespaces/{{project}}/domainmappings/{{name}}")
if err != nil {
return err
}

err = cloudRunPollingWaitTime(
config, res, project, waitURL, "Creating DomainMapping",
int(d.Timeout(schema.TimeoutCreate).Minutes()))

err = PollingWaitTime(resourceCloudRunDomainMappingPollRead(d, meta), PollCheckKnativeStatus, "Creating DomainMapping", d.Timeout(schema.TimeoutCreate))
if err != nil {
return fmt.Errorf("Error waiting to create DomainMapping: %s", err)
}
Expand All @@ -310,6 +303,39 @@ func resourceCloudRunDomainMappingCreate(d *schema.ResourceData, meta interface{
return resourceCloudRunDomainMappingRead(d, meta)
}

func resourceCloudRunDomainMappingPollRead(d *schema.ResourceData, meta interface{}) PollReadFunc {
return func() (map[string]interface{}, error) {
config := meta.(*Config)

url, err := replaceVars(d, config, "{{CloudRunBasePath}}apis/domains.cloudrun.com/v1/namespaces/{{project}}/domainmappings/{{name}}")
if err != nil {
return nil, err
}

project, err := getProject(d, config)
if err != nil {
return nil, err
}
res, err := sendRequest(config, "GET", project, url, nil)
if err != nil {
return res, err
}
res, err = resourceCloudRunDomainMappingDecoder(d, meta, res)
if err != nil {
return nil, err
}
if res == nil {
// Decoded object not found, spoof a 404 error for poll
return nil, &googleapi.Error{
Code: 404,
Message: "could not find object CloudRunDomainMapping",
}
}

return res, nil
}
}

func resourceCloudRunDomainMappingRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

Expand Down
56 changes: 37 additions & 19 deletions google/resource_cloud_run_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"time"

"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"google.golang.org/api/googleapi"
)

func resourceCloudRunService() *schema.Resource {
Expand Down Expand Up @@ -620,15 +621,7 @@ func resourceCloudRunServiceCreate(d *schema.ResourceData, meta interface{}) err
}
d.SetId(id)

waitURL, err := replaceVars(d, config, "{{CloudRunBasePath}}apis/serving.knative.dev/v1/namespaces/{{project}}/services/{{name}}")
if err != nil {
return err
}

err = cloudRunPollingWaitTime(
config, res, project, waitURL, "Creating Service",
int(d.Timeout(schema.TimeoutCreate).Minutes()))

err = PollingWaitTime(resourceCloudRunServicePollRead(d, meta), PollCheckKnativeStatus, "Creating Service", d.Timeout(schema.TimeoutCreate))
if err != nil {
return fmt.Errorf("Error waiting to create Service: %s", err)
}
Expand All @@ -638,6 +631,39 @@ func resourceCloudRunServiceCreate(d *schema.ResourceData, meta interface{}) err
return resourceCloudRunServiceRead(d, meta)
}

func resourceCloudRunServicePollRead(d *schema.ResourceData, meta interface{}) PollReadFunc {
return func() (map[string]interface{}, error) {
config := meta.(*Config)

url, err := replaceVars(d, config, "{{CloudRunBasePath}}apis/serving.knative.dev/v1/namespaces/{{project}}/services/{{name}}")
if err != nil {
return nil, err
}

project, err := getProject(d, config)
if err != nil {
return nil, err
}
res, err := sendRequest(config, "GET", project, url, nil)
if err != nil {
return res, err
}
res, err = resourceCloudRunServiceDecoder(d, meta, res)
if err != nil {
return nil, err
}
if res == nil {
// Decoded object not found, spoof a 404 error for poll
return nil, &googleapi.Error{
Code: 404,
Message: "could not find object CloudRunService",
}
}

return res, nil
}
}

func resourceCloudRunServiceRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)

Expand Down Expand Up @@ -724,21 +750,13 @@ func resourceCloudRunServiceUpdate(d *schema.ResourceData, meta interface{}) err
}

log.Printf("[DEBUG] Updating Service %q: %#v", d.Id(), obj)
res, err := sendRequestWithTimeout(config, "PUT", project, url, obj, d.Timeout(schema.TimeoutUpdate))
_, err = sendRequestWithTimeout(config, "PUT", project, url, obj, d.Timeout(schema.TimeoutUpdate))

if err != nil {
return fmt.Errorf("Error updating Service %q: %s", d.Id(), err)
}

waitURL, err := replaceVars(d, config, "{{CloudRunBasePath}}apis/serving.knative.dev/v1/namespaces/{{project}}/services/{{name}}")
if err != nil {
return err
}

err = cloudRunPollingWaitTime(
config, res, project, waitURL, "Updating Service",
int(d.Timeout(schema.TimeoutUpdate).Minutes()))

err = PollingWaitTime(resourceCloudRunServicePollRead(d, meta), PollCheckKnativeStatus, "Updating Service", d.Timeout(schema.TimeoutUpdate))
if err != nil {
return err
}
Expand Down
Loading