Skip to content
This repository has been archived by the owner on Aug 11, 2021. It is now read-only.

Add configurable timeout to wait_for #238

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/resources/kubernetes_manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ Once applied, the `object` attribute reflects the state of the resource as retur
### Nested Schema for `wait_for`

- **fields** (Map of String)
- **timeout** (string). Time to wait before timing out. Defaults to `10m`.


8 changes: 7 additions & 1 deletion provider/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,13 @@ func (s *RawProviderServer) ApplyResourceChange(ctx context.Context, req *tfprot
if ok {
err = s.waitForCompletion(ctx, wf, rs, rname, wt)
if err != nil {
return resp, err
resp.Diagnostics = append(resp.Diagnostics,
&tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Detail: err.Error(),
Summary: fmt.Sprintf(`wait_for: timed out waiting on resource %q`, rnn),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if there's some validation that can happen sooner, to prevent this error:

╷
│ Error: wait_for: timed out waiting on resource "default/tf-k8s-test-consul-server"

│ invalid field path "metadata.0.annotations.0.test": :1,10-11: Attribute name required; Dot must be followed by attribute
│ name.
╵

I used this as my config and it exited immediately:

  wait_for  = {
    timeout = "30s"
    fields  = {
      "metadata.0.annotations.0.test" = "true"
    }
  }

Also, is it possible for me to use an annotation to test this feature?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, it looks like you need to use [] for indexes. We've documented it in the format you're using though so something must have changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But yes – I'll add something to the ValidateResourceTypeConfig func to check for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have added validation so you get an error like this at plan:

╷
│ Error: Invalid "wait_for" configuration
│
│   with kubernetes_manifest.test,
│   on main.tf line 44, in resource "kubernetes_manifest" "test":
│   44:   wait_for = {
│   45:       fields = {
│   46:           "()*^B)*B^()^B)&*#^&$^#*&$TN&*#" = "lol"
│   47:       }
│   48:   }
│
│ invalid field path "()*^B)*B^()^B)&*#^&$^#*&$TN&*#": :1,4-5: Unsupported operator; Bitwise operators are not supported., and
│ 4 other diagnostic(s)
╵

})
return resp, nil
}
}

Expand Down
4 changes: 4 additions & 0 deletions provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,14 @@ func GetResourceType(name string) (tftypes.Type, error) {
func GetProviderResourceSchema() map[string]*tfprotov5.Schema {
waitForType := tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"timeout": tftypes.String,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to make this optional, but we're currently blocked because of this: hashicorp/terraform-plugin-go#83 (comment)

"fields": tftypes.Map{
AttributeType: tftypes.String,
},
},
OptionalAttributes: map[string]struct{}{
"timeout": {},
},
}

return map[string]*tfprotov5.Schema{
Expand Down
12 changes: 12 additions & 0 deletions provider/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,18 @@ func (s *RawProviderServer) ValidateResourceTypeConfig(ctx context.Context, req
}
}

// validate wait_for block
if v, ok := configVal["wait_for"]; ok && !v.IsNull() {
if err := s.waitForValidate(v); err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: `Invalid "wait_for" configuration`,
Detail: err.Error(),
Attribute: tftypes.NewAttributePath().WithAttributeName("wait_for"),
})
}
}

return resp, nil
}

Expand Down
59 changes: 56 additions & 3 deletions provider/waiter.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"math/big"
"regexp"
"time"

"github.com/hashicorp/go-hclog"
"github.com/hashicorp/terraform-plugin-go/tftypes"
Expand All @@ -18,6 +19,45 @@ import (
"k8s.io/client-go/dynamic"
)

const defaultWaitTimeout = "10m"

func (s *RawProviderServer) waitForValidate(waitForBlock tftypes.Value) error {
var waitForBlockVal map[string]tftypes.Value
err := waitForBlock.As(&waitForBlockVal)
if err != nil {
return err
}

if v, ok := waitForBlockVal["timeout"]; ok && !v.IsNull() {
var timeout string
v.As(&timeout)
_, err := time.ParseDuration(timeout)
if err != nil {
return err
}
}

fields, ok := waitForBlockVal["fields"]
if !ok || fields.IsNull() || !fields.IsKnown() {
return nil
}

if !fields.Type().Is(tftypes.Map{}) {
return fmt.Errorf(`"fields" should be a map of strings`)
}

var fieldsMap map[string]tftypes.Value
fields.As(&fieldsMap)
for k := range fieldsMap {
_, err := FieldPathToTftypesPath(k)
if err != nil {
return err
}
}

return nil
}

func (s *RawProviderServer) waitForCompletion(ctx context.Context, waitForBlock tftypes.Value, rs dynamic.ResourceInterface, rname string, rtype tftypes.Type) error {
if waitForBlock.IsNull() || !waitForBlock.IsKnown() {
return nil
Expand Down Expand Up @@ -79,12 +119,22 @@ func NewResourceWaiter(resource dynamic.ResourceInterface, resourceName string,
matchers = append(matchers, FieldMatcher{p, re})
}

t := defaultWaitTimeout
if tt, ok := waitForBlockVal["timeout"]; ok && !tt.IsNull() {
tt.As(&t)
}
timeout, err := time.ParseDuration(t)
if err != nil {
return nil, fmt.Errorf("wait_for: could not parse duration %q: %v", t, err)
}

return &FieldWaiter{
resource,
resourceName,
resourceType,
matchers,
hl,
timeout,
}, nil

}
Expand All @@ -103,15 +153,14 @@ type FieldWaiter struct {
resourceType tftypes.Type
fieldMatchers []FieldMatcher
logger hclog.Logger
timeout time.Duration
}

// Wait blocks until all of the FieldMatchers configured evaluate to true
func (w *FieldWaiter) Wait(ctx context.Context) error {
w.logger.Info("[ApplyResourceChange][Wait] Waiting until ready...\n")
end := time.Now().Add(w.timeout)
for {
// NOTE The typed API resource is actually returned in the
// event object but I haven't yet figured out how to convert it
// to a cty.Value.
res, err := w.resource.Get(ctx, w.resourceName, v1.GetOptions{})
if err != nil {
return err
Expand Down Expand Up @@ -174,6 +223,10 @@ func (w *FieldWaiter) Wait(ctx context.Context) error {
if done {
return err
}

if time.Now().After(end) {
return fmt.Errorf("wait_for: timed out after %s", w.timeout)
}
}
}

Expand Down