Skip to content

Commit 8b23492

Browse files
committed
add common kubernetes readiness checks
1 parent cb53f23 commit 8b23492

File tree

5 files changed

+382
-6
lines changed

5 files changed

+382
-6
lines changed

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,33 @@ env := testing.NewEnvironmentBuilder().
298298
env.ShouldReconcile(testing.RequestFromStrings("testresource"))
299299
```
300300

301+
### Readiness Checks
302+
303+
The `pkg/readiness` package provides a simple way to check if a kubernetes resource is ready.
304+
The meaning of readiness depends on the resource type.
305+
306+
#### Example
307+
308+
```go
309+
deployment := &appsv1.Deployment{}
310+
err := r.Client.Get(ctx, types.NamespacedName{
311+
Name: "my-deployment",
312+
Namespace: "my-namespace",
313+
}, deployment)
314+
315+
if err != nil {
316+
return err
317+
}
318+
319+
readiness := readiness.CheckDeployment(deployment)
320+
321+
if readiness.IsReady() {
322+
fmt.Println("Deployment is ready")
323+
} else {
324+
fmt.Printf("Deployment is not ready: %s\n", readiness.Message())
325+
}
326+
```
327+
301328
## Support, Feedback, Contributing
302329

303330
This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/openmcp-project/controller-utils/issues). Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md).

go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ require (
1919
k8s.io/client-go v0.33.0
2020
k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979
2121
sigs.k8s.io/controller-runtime v0.20.4
22+
sigs.k8s.io/gateway-api v1.3.0
2223
sigs.k8s.io/yaml v1.4.0
2324
)
2425

@@ -66,13 +67,13 @@ require (
6667
golang.org/x/time v0.10.0 // indirect
6768
golang.org/x/tools v0.33.0 // indirect
6869
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
69-
google.golang.org/protobuf v1.36.5 // indirect
70+
google.golang.org/protobuf v1.36.6 // indirect
7071
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
7172
gopkg.in/inf.v0 v0.9.1 // indirect
7273
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
7374
k8s.io/klog/v2 v2.130.1 // indirect
7475
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
7576
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
7677
sigs.k8s.io/randfill v1.0.0 // indirect
77-
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
78+
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
7879
)

go.sum

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,8 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
193193
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
194194
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
195195
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
196-
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
197-
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
196+
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
197+
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
198198
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
199199
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
200200
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -226,12 +226,14 @@ k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 h1:jgJW5IePPXLGB8e/1wvd0Ich9QE97
226226
k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
227227
sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU=
228228
sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY=
229+
sigs.k8s.io/gateway-api v1.3.0 h1:q6okN+/UKDATola4JY7zXzx40WO4VISk7i9DIfOvr9M=
230+
sigs.k8s.io/gateway-api v1.3.0/go.mod h1:d8NV8nJbaRbEKem+5IuxkL8gJGOZ+FJ+NvOIltV8gDk=
229231
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
230232
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
231233
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
232234
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
233235
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
234-
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
235-
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
236+
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI=
237+
sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
236238
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
237239
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

pkg/readiness/readinesscheck.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package readiness
2+
3+
import (
4+
"fmt"
5+
"slices"
6+
"strings"
7+
8+
appsv1 "k8s.io/api/apps/v1"
9+
batchv1 "k8s.io/api/batch/v1"
10+
networkingv1 "k8s.io/api/networking/v1"
11+
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
12+
)
13+
14+
type CheckResult []string
15+
16+
func (r CheckResult) IsReady() bool {
17+
return len(r) == 0
18+
}
19+
20+
func (r CheckResult) Message() string {
21+
return strings.Join(r, ", ")
22+
}
23+
24+
func NewReadyResult() CheckResult {
25+
return CheckResult{}
26+
}
27+
28+
func NewNotReadyResult(message string) CheckResult {
29+
return CheckResult{message}
30+
}
31+
32+
func NewFailedResult(err error) CheckResult {
33+
return NewNotReadyResult(fmt.Sprintf("readiness check failed: %v", err))
34+
}
35+
36+
func Aggregate(results ...CheckResult) CheckResult {
37+
return slices.Concat(results...)
38+
}
39+
40+
// CheckDeployment checks the readiness of a deployment.
41+
func CheckDeployment(dp *appsv1.Deployment) CheckResult {
42+
if dp.Status.ObservedGeneration < dp.Generation {
43+
return NewNotReadyResult(fmt.Sprintf("deployment %s/%s not ready: observed generation outdated", dp.Namespace, dp.Name))
44+
}
45+
46+
var specReplicas int32 = 0
47+
if dp.Spec.Replicas != nil {
48+
specReplicas = *dp.Spec.Replicas
49+
}
50+
51+
if dp.Generation != dp.Status.ObservedGeneration ||
52+
specReplicas != dp.Status.Replicas ||
53+
specReplicas != dp.Status.UpdatedReplicas ||
54+
specReplicas != dp.Status.AvailableReplicas {
55+
return NewNotReadyResult(fmt.Sprintf("deployment %s/%s is not ready", dp.Namespace, dp.Name))
56+
}
57+
58+
return NewReadyResult()
59+
}
60+
61+
// CheckJob checks the completion status of a Kubernetes Job.
62+
func CheckJob(job *batchv1.Job) CheckResult {
63+
for _, condition := range job.Status.Conditions {
64+
if condition.Type == batchv1.JobComplete && condition.Status == "True" {
65+
return NewReadyResult()
66+
}
67+
if condition.Type == batchv1.JobFailed && condition.Status == "True" {
68+
return NewNotReadyResult(fmt.Sprintf("job %s/%s failed: %s", job.Namespace, job.Name, condition.Message))
69+
}
70+
}
71+
72+
return NewNotReadyResult(fmt.Sprintf("job %s/%s is not completed", job.Namespace, job.Name))
73+
}
74+
75+
// CheckJobFailed checks if a Kubernetes Job has failed.
76+
func CheckJobFailed(job *batchv1.Job) bool {
77+
for _, condition := range job.Status.Conditions {
78+
if condition.Type == batchv1.JobFailed && condition.Status == "True" {
79+
return true
80+
}
81+
}
82+
return false
83+
}
84+
85+
// CheckIngress checks the readiness of a Kubernetes Ingress.
86+
func CheckIngress(ingress *networkingv1.Ingress) CheckResult {
87+
if len(ingress.Status.LoadBalancer.Ingress) > 0 {
88+
return NewReadyResult()
89+
}
90+
return NewNotReadyResult(fmt.Sprintf("ingress %s/%s is not ready: no load balancer ingress entries", ingress.Namespace, ingress.Name))
91+
}
92+
93+
// CheckGateway checks the readiness of a Kubernetes Gateway.
94+
func CheckGateway(gateway *gatewayv1.Gateway) CheckResult {
95+
for _, condition := range gateway.Status.Conditions {
96+
if condition.Type == string(gatewayv1.GatewayConditionReady) && condition.Status == "True" {
97+
return NewReadyResult()
98+
}
99+
}
100+
return NewNotReadyResult(fmt.Sprintf("gateway %s/%s is not ready: no ready condition with status True", gateway.Namespace, gateway.Name))
101+
}

0 commit comments

Comments
 (0)