Skip to content
Open
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/2851.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/kubernetes_manifest: Allow multiple wait types (fields, condition, rollout) to be used together in the wait block
```
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,7 @@ website/vendor
# output binary
terraform-provider-kubernetes
__debug_bin

# AI assistant rules
AGENTS.md
.bob/
36 changes: 34 additions & 2 deletions docs/resources/manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Optional:

Optional:

- `condition` (Block List) (see [below for nested schema](#nestedblock--wait--condition))
- `condition` (Block List) Wait for specific conditions to be met. Multiple condition blocks can be specified. (see [below for nested schema](#nestedblock--wait--condition))
- `fields` (Map of String) A map of paths to fields to wait for a specific field value.
- `rollout` (Boolean) Wait for rollout to complete on resources that support `kubectl rollout status`.

Expand Down Expand Up @@ -166,7 +166,7 @@ Note the import ID as the last argument to the import command. This ID points Te

## Using `wait` to block create and update calls

The `kubernetes_manifest` resource supports the ability to block create and update calls until a field is set or has a particular value by specifying the `wait` block. This is useful for when you create resources like Jobs and Services when you want to wait for something to happen after the resource is created by the API server before Terraform should consider the resource created.
The `kubernetes_manifest` resource supports the ability to block create and update calls until certain conditions are met by specifying the `wait` block. Multiple wait types can be combined in a single `wait` block, and they will be evaluated sequentially. This is useful for when you create resources like Jobs and Services when you want to wait for something to happen after the resource is created by the API server before Terraform should consider the resource created.

`wait` supports supports a `fields` attribute which allows you specify a map of fields paths to regular expressions. You can also specify `*` if you just want to wait for a field to have any value.

Expand Down Expand Up @@ -231,6 +231,38 @@ resource "kubernetes_manifest" "test" {
}
```

## Combining Multiple Wait Types

You can combine `fields`, `condition`, and `rollout` in a single `wait` block. The provider will evaluate them sequentially in the order: rollout → conditions → fields.

```terraform
resource "kubernetes_manifest" "test" {
manifest = {
// ...
}

wait {
# Wait for specific field values
fields = {
"status.phase" = "Running"
}


# Wait for multiple conditions
condition {
type = "Ready"
status = "True"
}

condition {
type = "ContainersReady"
status = "True"
}
}
}
```


## Configuring `field_manager`

The `kubernetes_manifest` exposes configuration of the field manager through the optional `field_manager` block.
Expand Down
11 changes: 0 additions & 11 deletions manifest/provider/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package provider
import (
"context"
"fmt"
"strings"
"time"

"github.com/hashicorp/terraform-plugin-go/tfprotov5"
Expand Down Expand Up @@ -128,7 +127,6 @@ func (s *RawProviderServer) ValidateResourceTypeConfig(ctx context.Context, req
if len(waitBlock) > 0 {
var w map[string]tftypes.Value
waitBlock[0].As(&w)
waiters := []string{}
for k, ww := range w {
if !ww.IsNull() {
if k == "condition" {
Expand All @@ -138,17 +136,8 @@ func (s *RawProviderServer) ValidateResourceTypeConfig(ctx context.Context, req
continue
}
}
waiters = append(waiters, k)
}
}
if len(waiters) > 1 {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Invalid wait configuration",
Detail: fmt.Sprintf(`You may only set one of "%s".`, strings.Join(waiters, "\", \"")),
Attribute: tftypes.NewAttributePath().WithAttributeName("wait"),
})
}
}
}
if waitFor, ok := configVal["wait_for"]; ok && !waitFor.IsNull() {
Expand Down
108 changes: 69 additions & 39 deletions manifest/provider/waiter.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,76 +58,86 @@ func NewResourceWaiter(resource dynamic.ResourceInterface, resourceName string,
return nil, err
}

var waiters []Waiter

if v, ok := waitForBlockVal["rollout"]; ok {
var rollout bool
v.As(&rollout)
if rollout {
return &RolloutWaiter{
waiters = append(waiters, &RolloutWaiter{
resource,
resourceName,
hl,
}, nil
})
}
}

if v, ok := waitForBlockVal["condition"]; ok {
var conditionsBlocks []tftypes.Value
v.As(&conditionsBlocks)
if len(conditionsBlocks) > 0 {
return &ConditionsWaiter{
waiters = append(waiters, &ConditionsWaiter{
resource,
resourceName,
conditionsBlocks,
hl,
}, nil
})
}
}

fields, ok := waitForBlockVal["fields"]
if !ok || fields.IsNull() || !fields.IsKnown() {
return &NoopWaiter{}, nil
}
if ok && !fields.IsNull() && fields.IsKnown() {
if !fields.Type().Is(tftypes.Map{}) {
return nil, fmt.Errorf(`"fields" should be a map of strings`)
}

if !fields.Type().Is(tftypes.Map{}) {
return nil, fmt.Errorf(`"fields" should be a map of strings`)
}
var vm map[string]tftypes.Value
fields.As(&vm)
var matchers []FieldMatcher

for k, v := range vm {
var expr string
v.As(&expr)
var re *regexp.Regexp
if expr == "*" {
// NOTE this is just a shorthand so the user doesn't have to
// type the expression below all the time
re = regexp.MustCompile("(.*)?")
} else {
var err error
re, err = regexp.Compile(expr)
if err != nil {
return nil, fmt.Errorf("invalid regular expression: %q", expr)
}
}

var vm map[string]tftypes.Value
fields.As(&vm)
var matchers []FieldMatcher

for k, v := range vm {
var expr string
v.As(&expr)
var re *regexp.Regexp
if expr == "*" {
// NOTE this is just a shorthand so the user doesn't have to
// type the expression below all the time
re = regexp.MustCompile("(.*)?")
} else {
var err error
re, err = regexp.Compile(expr)
p, err := FieldPathToTftypesPath(k)
if err != nil {
return nil, fmt.Errorf("invalid regular expression: %q", expr)
return nil, err
}
matchers = append(matchers, FieldMatcher{p, re})
}

p, err := FieldPathToTftypesPath(k)
if err != nil {
return nil, err
}
matchers = append(matchers, FieldMatcher{p, re})
waiters = append(waiters, &FieldWaiter{
resource,
resourceName,
resourceType,
th,
matchers,
hl,
})
}

return &FieldWaiter{
resource,
resourceName,
resourceType,
th,
matchers,
hl,
}, nil
// Return appropriate waiter based on count
if len(waiters) == 0 {
return &NoopWaiter{}, nil
}

if len(waiters) == 1 {
return waiters[0], nil
}

return &CompositeWaiter{waiters: waiters, logger: hl}, nil
}

// FieldMatcher contains a tftypes.AttributePath to a field and a regexp to match on it
Expand Down Expand Up @@ -237,6 +247,26 @@ func (w *NoopWaiter) Wait(_ context.Context) error {
return nil
}

// CompositeWaiter executes multiple waiters in sequence
type CompositeWaiter struct {
waiters []Waiter
logger hclog.Logger
}

// Wait executes all waiters in sequence
func (w *CompositeWaiter) Wait(ctx context.Context) error {
w.logger.Info("[ApplyResourceChange][Wait] Starting composite wait...")
for i, waiter := range w.waiters {
w.logger.Info(fmt.Sprintf("[ApplyResourceChange][Wait] Executing waiter %d/%d", i+1, len(w.waiters)))
err := waiter.Wait(ctx)
if err != nil {
return fmt.Errorf("waiter %d failed: %w", i+1, err)
}
}
w.logger.Info("[ApplyResourceChange][Wait] All waiters completed successfully")
return nil
}

// FieldPathToTftypesPath takes a string representation of
// a path to a field in dot/square bracket notation
// and returns a tftypes.AttributePath
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Copyright IBM Corp. 2017, 2026
# SPDX-License-Identifier: MPL-2.0


resource "kubernetes_manifest" "test" {

manifest = {
apiVersion = "v1"
kind = "Pod"

metadata = {
name = var.name
namespace = var.namespace

annotations = {
"test.terraform.io" = "test"
}

labels = {
app = "nginx"
}
}

spec = {
containers = [
{
name = "nginx"
image = "nginx:1.19"

readinessProbe = {
initialDelaySeconds = 10

httpGet = {
path = "/"
port = 80
}
}
}
]
}
}

wait {
fields = {
"status.phase" = "Running"
}

condition {
type = "Ready"
status = "True"
}

condition {
type = "ContainersReady"
status = "True"
}
}
}
62 changes: 62 additions & 0 deletions manifest/test/acceptance/wait_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,65 @@ func TestKubernetesManifest_WaitFields_Annotations(t *testing.T) {

tfstate.AssertOutputExists(t, "test")
}

func TestKubernetesManifest_WaitMultipleTypes_Pod(t *testing.T) {
ctx := context.Background()

name := randName()
namespace := randName()

reattachInfo, err := provider.ServeTest(ctx, hclog.Default(), t)
if err != nil {
t.Errorf("Failed to create provider instance: %q", err)
}

tf := tfhelper.RequireNewWorkingDir(ctx, t)
tf.SetReattachInfo(ctx, reattachInfo)
defer func() {
tf.Destroy(ctx)
tf.Close()
k8shelper.AssertNamespacedResourceDoesNotExist(t, "v1", "pods", namespace, name)
}()

k8shelper.CreateNamespace(t, namespace)
defer k8shelper.DeleteResource(t, namespace, kubernetes.NewGroupVersionResource("v1", "namespaces"))

tfvars := TFVARS{
"namespace": namespace,
"name": name,
}
tfconfig := loadTerraformConfig(t, "Wait/wait_for_multiple_types.tf", tfvars)
tf.SetConfig(ctx, tfconfig)
tf.Init(ctx)

startTime := time.Now()
err = tf.Apply(ctx)
if err != nil {
t.Fatalf("Failed to apply: %q", err)
}

k8shelper.AssertNamespacedResourceExists(t, "v1", "pods", namespace, name)

// NOTE We set a readinessProbe in the fixture with a delay of 10s
// so the apply should take at least 10 seconds to complete.
minDuration := time.Duration(10) * time.Second
applyDuration := time.Since(startTime)
if applyDuration < minDuration {
t.Fatalf("the apply should have taken at least %s", minDuration)
}

st, err := tf.State(ctx)
if err != nil {
t.Fatalf("Failed to get state: %q", err)
}
tfstate := tfstatehelper.NewHelper(st)
tfstate.AssertAttributeValues(t, tfstatehelper.AttributeValues{
"kubernetes_manifest.test.wait.0.fields": map[string]interface{}{
"status.phase": "Running",
},
"kubernetes_manifest.test.wait.0.condition.0.type": "Ready",
"kubernetes_manifest.test.wait.0.condition.0.status": "True",
"kubernetes_manifest.test.wait.0.condition.1.type": "ContainersReady",
"kubernetes_manifest.test.wait.0.condition.1.status": "True",
})
}
Loading