Skip to content
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
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,33 @@ env := testing.NewEnvironmentBuilder().
env.ShouldReconcile(testing.RequestFromStrings("testresource"))
```

### Readiness Checks

The `pkg/readiness` package provides a simple way to check if a kubernetes resource is ready.
The meaning of readiness depends on the resource type.

#### Example

```go
deployment := &appsv1.Deployment{}
err := r.Client.Get(ctx, types.NamespacedName{
Name: "my-deployment",
Namespace: "my-namespace",
}, deployment)

if err != nil {
return err
}

readiness := readiness.CheckDeployment(deployment)

if readiness.IsReady() {
fmt.Println("Deployment is ready")
} else {
fmt.Printf("Deployment is not ready: %s\n", readiness.Message())
}
```

### Kubernetes resource management

The `pkg/resource` package contains some useful functions for working with Kubernetes resources. The `Mutator` interface can be used to modify resources in a generic way. It is used by the `Mutate` function, which takes a resource and a mutator and applies the mutator to the resource.
Expand Down Expand Up @@ -412,7 +439,6 @@ func ReconcileResources(ctx context.Context, client client.Client) error {

return nil
}

```

## Support, Feedback, Contributing
Expand Down
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ require (
k8s.io/client-go v0.33.0
k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979
sigs.k8s.io/controller-runtime v0.20.4
sigs.k8s.io/gateway-api v1.3.0
sigs.k8s.io/yaml v1.4.0
)

Expand Down Expand Up @@ -66,13 +67,13 @@ require (
golang.org/x/time v0.10.0 // indirect
golang.org/x/tools v0.33.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
)
10 changes: 6 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,8 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
Expand Down Expand Up @@ -226,12 +226,14 @@ k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 h1:jgJW5IePPXLGB8e/1wvd0Ich9QE97
k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU=
sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY=
sigs.k8s.io/gateway-api v1.3.0 h1:q6okN+/UKDATola4JY7zXzx40WO4VISk7i9DIfOvr9M=
sigs.k8s.io/gateway-api v1.3.0/go.mod h1:d8NV8nJbaRbEKem+5IuxkL8gJGOZ+FJ+NvOIltV8gDk=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI=
sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
101 changes: 101 additions & 0 deletions pkg/readiness/readinesscheck.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package readiness

import (
"fmt"
"slices"
"strings"

appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
networkingv1 "k8s.io/api/networking/v1"
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
)

type CheckResult []string

func (r CheckResult) IsReady() bool {
return len(r) == 0
}

func (r CheckResult) Message() string {
return strings.Join(r, ", ")
}

func NewReadyResult() CheckResult {
return CheckResult{}
}

func NewNotReadyResult(message string) CheckResult {
return CheckResult{message}
}

func NewFailedResult(err error) CheckResult {
return NewNotReadyResult(fmt.Sprintf("readiness check failed: %v", err))
}

func Aggregate(results ...CheckResult) CheckResult {
return slices.Concat(results...)
}

// CheckDeployment checks the readiness of a deployment.
func CheckDeployment(dp *appsv1.Deployment) CheckResult {
if dp.Status.ObservedGeneration < dp.Generation {
return NewNotReadyResult(fmt.Sprintf("deployment %s/%s not ready: observed generation outdated", dp.Namespace, dp.Name))
}

var specReplicas int32 = 0
if dp.Spec.Replicas != nil {
specReplicas = *dp.Spec.Replicas
}

if dp.Generation != dp.Status.ObservedGeneration ||
specReplicas != dp.Status.Replicas ||
specReplicas != dp.Status.UpdatedReplicas ||
specReplicas != dp.Status.AvailableReplicas {
return NewNotReadyResult(fmt.Sprintf("deployment %s/%s is not ready", dp.Namespace, dp.Name))
}

return NewReadyResult()
}

// CheckJob checks the completion status of a Kubernetes Job.
func CheckJob(job *batchv1.Job) CheckResult {
for _, condition := range job.Status.Conditions {
if condition.Type == batchv1.JobComplete && condition.Status == "True" {
return NewReadyResult()
}
if condition.Type == batchv1.JobFailed && condition.Status == "True" {
return NewNotReadyResult(fmt.Sprintf("job %s/%s failed: %s", job.Namespace, job.Name, condition.Message))
}
}

return NewNotReadyResult(fmt.Sprintf("job %s/%s is not completed", job.Namespace, job.Name))
}

// CheckJobFailed checks if a Kubernetes Job has failed.
func CheckJobFailed(job *batchv1.Job) bool {
for _, condition := range job.Status.Conditions {
if condition.Type == batchv1.JobFailed && condition.Status == "True" {
return true
}
}
return false
}

// CheckIngress checks the readiness of a Kubernetes Ingress.
func CheckIngress(ingress *networkingv1.Ingress) CheckResult {
if len(ingress.Status.LoadBalancer.Ingress) > 0 {
return NewReadyResult()
}
return NewNotReadyResult(fmt.Sprintf("ingress %s/%s is not ready: no load balancer ingress entries", ingress.Namespace, ingress.Name))
}

// CheckGateway checks the readiness of a Kubernetes Gateway.
func CheckGateway(gateway *gatewayv1.Gateway) CheckResult {
for _, condition := range gateway.Status.Conditions {
if condition.Type == string(gatewayv1.GatewayConditionReady) && condition.Status == "True" {
return NewReadyResult()
}
}
return NewNotReadyResult(fmt.Sprintf("gateway %s/%s is not ready: no ready condition with status True", gateway.Namespace, gateway.Name))
}
Loading