Skip to content

Commit 3d31174

Browse files
committed
add initialization.kcp.io/wait-for-ready annotation on manifests to wait for them to become ready
On-behalf-of: @SAP christoph.mewes@sap.com
1 parent 1cba14d commit 3d31174

File tree

5 files changed

+522
-9
lines changed

5 files changed

+522
-9
lines changed

internal/manifest/applier.go

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import (
2222
"strings"
2323

2424
"github.com/kcp-dev/init-agent/internal/log"
25+
"github.com/kcp-dev/init-agent/sdk/types"
26+
"go.uber.org/zap"
2527

2628
apierrors "k8s.io/apimachinery/pkg/api/errors"
2729
"k8s.io/apimachinery/pkg/api/meta"
@@ -53,18 +55,38 @@ func (a *applier) Apply(ctx context.Context, client ctrlruntimeclient.Client, ob
5355
}
5456
}
5557

56-
return false, nil
57-
}
58+
// After creating objects, check readiness of annotated ones
59+
for _, object := range objs {
60+
conditionType := object.GetAnnotations()[types.WaitForReadyAnnotation]
61+
if conditionType == "" {
62+
continue
63+
}
5864

59-
func (a *applier) applyObject(ctx context.Context, client ctrlruntimeclient.Client, obj *unstructured.Unstructured) error {
60-
gvk := obj.GroupVersionKind()
65+
// Fetch current state
66+
current := &unstructured.Unstructured{}
67+
current.SetGroupVersionKind(object.GroupVersionKind())
6168

62-
key := ctrlruntimeclient.ObjectKeyFromObject(obj).String()
63-
// make key look prettier for cluster-scoped objects
64-
key = strings.TrimLeft(key, "/")
69+
if err := client.Get(ctx, ctrlruntimeclient.ObjectKeyFromObject(object), current); err != nil {
70+
if apierrors.IsNotFound(err) {
71+
requeue = true
72+
continue
73+
}
74+
return false, err
75+
}
6576

66-
logger := log.FromContext(ctx)
67-
logger.Debugw("Applying object", "obj-key", key, "obj-gvk", gvk)
77+
if !HasCondition(current, conditionType) {
78+
logger := a.objectLogger(ctx, object)
79+
logger.Debugw("Waiting for condition", "condition", conditionType)
80+
requeue = true
81+
}
82+
}
83+
84+
return requeue, nil
85+
}
86+
87+
func (a *applier) applyObject(ctx context.Context, client ctrlruntimeclient.Client, obj *unstructured.Unstructured) error {
88+
logger := a.objectLogger(ctx, obj)
89+
logger.Debugw("Applying object")
6890

6991
if err := client.Create(ctx, obj); err != nil {
7092
if !apierrors.IsAlreadyExists(err) {
@@ -74,3 +96,13 @@ func (a *applier) applyObject(ctx context.Context, client ctrlruntimeclient.Clie
7496

7597
return nil
7698
}
99+
100+
func (a *applier) objectLogger(ctx context.Context, obj *unstructured.Unstructured) *zap.SugaredLogger {
101+
gvk := obj.GroupVersionKind()
102+
103+
key := ctrlruntimeclient.ObjectKeyFromObject(obj).String()
104+
// make key look prettier for cluster-scoped objects
105+
key = strings.TrimLeft(key, "/")
106+
107+
return log.FromContext(ctx).With("obj-key", key, "obj-gvk", gvk)
108+
}

internal/manifest/readiness.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
Copyright 2026 The kcp Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package manifest
18+
19+
import (
20+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
21+
)
22+
23+
// HasCondition checks if an unstructured object has the specified condition
24+
// type with status "True".
25+
func HasCondition(obj *unstructured.Unstructured, conditionType string) bool {
26+
conditions, found, err := unstructured.NestedSlice(obj.Object, "status", "conditions")
27+
if err != nil || !found {
28+
return false
29+
}
30+
31+
for _, c := range conditions {
32+
condition, ok := c.(map[string]any)
33+
if !ok {
34+
continue
35+
}
36+
37+
cType, _, _ := unstructured.NestedString(condition, "type")
38+
cStatus, _, _ := unstructured.NestedString(condition, "status")
39+
if cType == conditionType && cStatus == "True" {
40+
return true
41+
}
42+
}
43+
44+
return false
45+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
Copyright 2026 The kcp Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package manifest
18+
19+
import (
20+
"testing"
21+
22+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
23+
)
24+
25+
func TestHasCondition(t *testing.T) {
26+
testcases := []struct {
27+
name string
28+
obj *unstructured.Unstructured
29+
conditionType string
30+
expected bool
31+
}{
32+
{
33+
name: "no status",
34+
obj: newUnstructured("v1", "ConfigMap", "test"),
35+
conditionType: "Ready",
36+
expected: false,
37+
},
38+
{
39+
name: "no conditions",
40+
obj: newUnstructuredWithStatus("v1", "ConfigMap", "test", map[string]any{}),
41+
conditionType: "Ready",
42+
expected: false,
43+
},
44+
{
45+
name: "empty conditions",
46+
obj: newUnstructuredWithConditions("v1", "ConfigMap", "test", []any{}),
47+
conditionType: "Ready",
48+
expected: false,
49+
},
50+
{
51+
name: "condition type not found",
52+
obj: newUnstructuredWithConditions("v1", "ConfigMap", "test", []any{
53+
map[string]any{"type": "Available", "status": "True"},
54+
}),
55+
conditionType: "Ready",
56+
expected: false,
57+
},
58+
{
59+
name: "condition found but status is False",
60+
obj: newUnstructuredWithConditions("v1", "ConfigMap", "test", []any{
61+
map[string]any{"type": "Ready", "status": "False"},
62+
}),
63+
conditionType: "Ready",
64+
expected: false,
65+
},
66+
{
67+
name: "condition found but status is Unknown",
68+
obj: newUnstructuredWithConditions("v1", "ConfigMap", "test", []any{
69+
map[string]any{"type": "Ready", "status": "Unknown"},
70+
}),
71+
conditionType: "Ready",
72+
expected: false,
73+
},
74+
{
75+
name: "condition found with status True",
76+
obj: newUnstructuredWithConditions("v1", "ConfigMap", "test", []any{
77+
map[string]any{"type": "Ready", "status": "True"},
78+
}),
79+
conditionType: "Ready",
80+
expected: true,
81+
},
82+
{
83+
name: "multiple conditions - target is True",
84+
obj: newUnstructuredWithConditions("v1", "ConfigMap", "test", []any{
85+
map[string]any{"type": "Available", "status": "True"},
86+
map[string]any{"type": "Ready", "status": "True"},
87+
map[string]any{"type": "Progressing", "status": "False"},
88+
}),
89+
conditionType: "Ready",
90+
expected: true,
91+
},
92+
{
93+
name: "multiple conditions - target is False",
94+
obj: newUnstructuredWithConditions("v1", "ConfigMap", "test", []any{
95+
map[string]any{"type": "Available", "status": "True"},
96+
map[string]any{"type": "Ready", "status": "False"},
97+
map[string]any{"type": "Progressing", "status": "True"},
98+
}),
99+
conditionType: "Ready",
100+
expected: false,
101+
},
102+
{
103+
name: "CRD Established condition True",
104+
obj: newUnstructuredWithConditions("apiextensions.k8s.io/v1", "CustomResourceDefinition", "test", []any{
105+
map[string]any{"type": "NamesAccepted", "status": "True"},
106+
map[string]any{"type": "Established", "status": "True"},
107+
}),
108+
conditionType: "Established",
109+
expected: true,
110+
},
111+
{
112+
name: "CRD Established condition False",
113+
obj: newUnstructuredWithConditions("apiextensions.k8s.io/v1", "CustomResourceDefinition", "test", []any{
114+
map[string]any{"type": "NamesAccepted", "status": "True"},
115+
map[string]any{"type": "Established", "status": "False"},
116+
}),
117+
conditionType: "Established",
118+
expected: false,
119+
},
120+
{
121+
name: "malformed condition entry (not a map)",
122+
obj: newUnstructuredWithConditions("v1", "ConfigMap", "test", []any{
123+
"not a map",
124+
map[string]any{"type": "Ready", "status": "True"},
125+
}),
126+
conditionType: "Ready",
127+
expected: true,
128+
},
129+
}
130+
131+
for _, tt := range testcases {
132+
t.Run(tt.name, func(t *testing.T) {
133+
result := HasCondition(tt.obj, tt.conditionType)
134+
if result != tt.expected {
135+
t.Fatalf("Expected %v.", tt.expected)
136+
}
137+
})
138+
}
139+
}
140+
141+
func newUnstructuredWithStatus(apiVersion, kind, name string, status map[string]any) *unstructured.Unstructured {
142+
obj := newUnstructured(apiVersion, kind, name)
143+
obj.Object["status"] = status
144+
return obj
145+
}
146+
147+
func newUnstructuredWithConditions(apiVersion, kind, name string, conditions []any) *unstructured.Unstructured {
148+
return newUnstructuredWithStatus(apiVersion, kind, name, map[string]any{
149+
"conditions": conditions,
150+
})
151+
}

sdk/types/annotations.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
Copyright 2026 The kcp Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package types
18+
19+
const (
20+
// WaitForReadyAnnotation specifies a condition type to wait for.
21+
// The value is the condition type name (e.g., "Ready", "Established").
22+
// The agent will wait until that condition has status=True.
23+
WaitForReadyAnnotation = "initialization.kcp.io/wait-for-ready"
24+
)

0 commit comments

Comments
 (0)