Skip to content

Commit 7b2babf

Browse files
enable updates to probes in gateways (#4901)
* demo code * Update Makefile for local development; enhance deployment probe handling * Create 4901.txt * Update Consul image to use correct version * Add support for Kubernetes health probes in managed Gateway classes - Introduced configuration for liveness, readiness, and startup probes in GatewayClassConfig. - Updated related templates and values to handle probe configurations. - Enhanced command to load probes from a config map. * Add tests for probe propagation and sanitization in GatewayClassConfig * Improve error handling and sanitization in loadProbesConfig function * Refactor Gateway Probes Configuration Introduced a new mechanism to extract probe configurations directly from Gateway annotations. * Remove unused transformGatewayClassConfig function from GatewayController * Fix failing unit tests * Fix comment formatting in ProbesConfig definition * Refactor serializeGatewayClassConfig to ensure annotation reflects the latest GatewayClassConfig
1 parent f45e027 commit 7b2babf

File tree

11 files changed

+729
-15
lines changed

11 files changed

+729
-15
lines changed

.changelog/4901.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:feature
2+
api-gateway: Add support for configuring Kubernetes probes (liveness, readiness, startup) per-Gateway via annotations. Use `consul.hashicorp.com/liveness-probe`, `consul.hashicorp.com/readiness-probe`, and `consul.hashicorp.com/startup-probe` annotations with JSON probe configuration to customize health checks for individual API Gateways. [[GH-4901](https://github.com/hashicorp/consul-k8s/pull/4901)]
3+
```

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ control-plane-dev-docker: ## Build consul-k8s-control-plane dev Docker image.
5555
--build-arg 'GIT_COMMIT=$(GIT_COMMIT)' \
5656
--build-arg 'GIT_DIRTY=$(GIT_DIRTY)' \
5757
--build-arg 'GIT_DESCRIBE=$(GIT_DESCRIBE)' \
58-
-f $(CURDIR)/control-plane/Dockerfile $(CURDIR)/control-plane
58+
-f $(CURDIR)/control-plane/Dockerfile $(CURDIR)/control-plane --load
5959

6060
.PHONY: control-plane-dev-skaffold
6161
# DANGER: this target is experimental and could be modified/removed at any time.

charts/consul/README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,69 @@ Please see the many options supported in the `values.yaml`
103103
file. These are also fully documented directly on the
104104
[Consul website](https://www.consul.io/docs/platform/k8s/helm.html).
105105
106+
## API Gateway Probe Configuration
107+
108+
You can configure Kubernetes health probes (liveness, readiness, and startup) for individual API Gateways using annotations. This allows you to customize probe behavior per-Gateway rather than using a class-wide configuration.
109+
110+
### Example Gateway with Custom Probes
111+
112+
```yaml
113+
apiVersion: gateway.networking.k8s.io/v1beta1
114+
kind: Gateway
115+
metadata:
116+
name: example-gateway
117+
namespace: consul
118+
annotations:
119+
# Configure liveness probe (JSON format)
120+
consul.hashicorp.com/liveness-probe: |
121+
{
122+
"httpGet": {
123+
"path": "/ready",
124+
"port": 20000
125+
},
126+
"initialDelaySeconds": 10,
127+
"periodSeconds": 10,
128+
"timeoutSeconds": 1,
129+
"successThreshold": 1,
130+
"failureThreshold": 3
131+
}
132+
# Configure readiness probe (JSON format)
133+
consul.hashicorp.com/readiness-probe: |
134+
{
135+
"httpGet": {
136+
"path": "/ready",
137+
"port": 20000
138+
},
139+
"initialDelaySeconds": 5,
140+
"periodSeconds": 10
141+
}
142+
# Configure startup probe (JSON format)
143+
consul.hashicorp.com/startup-probe: |
144+
{
145+
"tcpSocket": {
146+
"port": 20000
147+
},
148+
"periodSeconds": 2,
149+
"failureThreshold": 30
150+
}
151+
spec:
152+
gatewayClassName: consul
153+
listeners:
154+
- name: http
155+
protocol: HTTP
156+
port: 8080
157+
```
158+
159+
### Supported Probe Annotations
160+
161+
- `consul.hashicorp.com/liveness-probe`: Liveness probe configuration (JSON)
162+
- `consul.hashicorp.com/readiness-probe`: Readiness probe configuration (JSON)
163+
- `consul.hashicorp.com/startup-probe`: Startup probe configuration (JSON)
164+
165+
Each annotation accepts a JSON object following the Kubernetes [Probe](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#probe-v1-core) specification. Supported probe handlers: `httpGet`, `tcpSocket`, `exec`, `grpc`.
166+
167+
**Note**: Liveness and startup probes must have `successThreshold: 1` per Kubernetes requirements. The controller will automatically normalize this value if a different value is provided.
168+
106169
## Tutorials
107170
108171
You can find examples and complete tutorials on how to deploy Consul on

charts/consul/templates/crd-gatewayclassconfigs-v1.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
{{- if .Values.connectInject.enabled }}
2+
# Copyright (c) HashiCorp, Inc.
3+
# SPDX-License-Identifier: MPL-2.0
4+
25
apiVersion: apiextensions.k8s.io/v1
36
kind: CustomResourceDefinition
47
metadata:
58
annotations:
69
controller-gen.kubebuilder.io/version: v0.14.0
10+
name: gatewayclassconfigs.consul.hashicorp.com
711
labels:
812
app: {{ template "consul.name" . }}
913
chart: {{ template "consul.chart" . }}
1014
heritage: {{ .Release.Service }}
1115
release: {{ .Release.Name }}
1216
component: crd
13-
name: gatewayclassconfigs.consul.hashicorp.com
1417
spec:
1518
group: consul.hashicorp.com
1619
names:

cli/helm/values.go

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -578,13 +578,55 @@ type CopyAnnotations struct {
578578
}
579579

580580
type ManagedGatewayClass struct {
581-
Enabled bool `yaml:"enabled"`
582-
NodeSelector interface{} `yaml:"nodeSelector"`
583-
ServiceType string `yaml:"serviceType"`
584-
UseHostPorts bool `yaml:"useHostPorts"`
585-
CopyAnnotations CopyAnnotations `yaml:"copyAnnotations"`
586-
OpenshiftSCCName string `yaml:"openshiftSCCName"`
587-
MapPrivilegedContainerPorts int `yaml:"mapPrivilegedContainerPorts"`
581+
Enabled bool `yaml:"enabled"`
582+
NodeSelector interface{} `yaml:"nodeSelector"`
583+
ServiceType string `yaml:"serviceType"`
584+
UseHostPorts bool `yaml:"useHostPorts"`
585+
CopyAnnotations CopyAnnotations `yaml:"copyAnnotations"`
586+
OpenshiftSCCName string `yaml:"openshiftSCCName"`
587+
MapPrivilegedContainerPorts int `yaml:"mapPrivilegedContainerPorts"`
588+
Probes ManagedGatewayClassProbes `yaml:"probes"`
589+
}
590+
591+
// ProbeHTTPGet models the HTTP GET action of a Kubernetes Probe.
592+
type ProbeHTTPGet struct {
593+
Path string `yaml:"path"`
594+
Port interface{} `yaml:"port"` // int or string (named port)
595+
Host string `yaml:"host"`
596+
Scheme string `yaml:"scheme"`
597+
// Headers intentionally omitted for now; can be added if needed.
598+
}
599+
600+
// ProbeTCPSocket models the TCP socket action of a Kubernetes Probe.
601+
type ProbeTCPSocket struct {
602+
Port interface{} `yaml:"port"` // int or string
603+
Host string `yaml:"host"`
604+
}
605+
606+
// ProbeExec models the exec action of a Kubernetes Probe.
607+
type ProbeExec struct {
608+
Command []string `yaml:"command"`
609+
}
610+
611+
// ProbeSpec is a simplified Kubernetes-style Probe configuration allowing http, tcp, or exec.
612+
// Only one of HTTPGet, TCPSocket, or Exec should be set. Enabled defaults to true if any action is specified.
613+
type ProbeSpec struct {
614+
Enabled *bool `yaml:"enabled"`
615+
HTTPGet *ProbeHTTPGet `yaml:"httpGet"`
616+
TCPSocket *ProbeTCPSocket `yaml:"tcpSocket"`
617+
Exec *ProbeExec `yaml:"exec"`
618+
InitialDelaySeconds int `yaml:"initialDelaySeconds"`
619+
PeriodSeconds int `yaml:"periodSeconds"`
620+
TimeoutSeconds int `yaml:"timeoutSeconds"`
621+
SuccessThreshold int `yaml:"successThreshold"`
622+
FailureThreshold int `yaml:"failureThreshold"`
623+
}
624+
625+
// ManagedGatewayClassProbes groups the three standard Kubernetes probe types applied to gateway pods.
626+
type ManagedGatewayClassProbes struct {
627+
Liveness ProbeSpec `yaml:"liveness"`
628+
Readiness ProbeSpec `yaml:"readiness"`
629+
Startup ProbeSpec `yaml:"startup"`
588630
}
589631

590632
type Service struct {

control-plane/api-gateway/binding/annotations_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ func TestSerializeGatewayClassConfig_HappyPath(t *testing.T) {
112112
ObjectMeta: metav1.ObjectMeta{
113113
Name: "my-gw",
114114
Annotations: map[string]string{
115-
common.AnnotationGatewayClassConfig: `{"serviceType":"serviceType","nodeSelector":{"selector":"of node"},"tolerations":[{"key":"key","operator":"op","value":"120","effect":"to the moon","tolerationSeconds":0}],"copyAnnotations":{"service":["service"]}}`,
115+
common.AnnotationGatewayClassConfig: `{"serviceType":"serviceType","nodeSelector":{"selector":"of node"},"tolerations":[{"key":"key","operator":"op","value":"120","effect":"to the moon","tolerationSeconds":0}],"deployment":{},"copyAnnotations":{"service":["service"]},"metrics":{}}`,
116116
},
117117
},
118118
Spec: gwv1beta1.GatewaySpec{},

control-plane/api-gateway/gatekeeper/deployment.go

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"strconv"
99

10+
"github.com/go-logr/logr"
1011
"github.com/google/go-cmp/cmp"
1112
appsv1 "k8s.io/api/apps/v1"
1213
corev1 "k8s.io/api/core/v1"
@@ -59,7 +60,7 @@ func (g *Gatekeeper) upsertDeployment(ctx context.Context, gateway gwv1beta1.Gat
5960
}
6061

6162
mutated := deployment.DeepCopy()
62-
mutator := newDeploymentMutator(deployment, mutated, existingDeployment, exists, gcc, gateway, g.Client.Scheme())
63+
mutator := newDeploymentMutator(deployment, mutated, existingDeployment, exists, gcc, gateway, g.Client.Scheme(), g.Log)
6364

6465
result, err := controllerutil.CreateOrUpdate(ctx, g.Client, mutated, mutator)
6566
if err != nil {
@@ -162,12 +163,35 @@ func (g *Gatekeeper) deployment(gateway gwv1beta1.Gateway, gcc v1alpha1.GatewayC
162163
}, nil
163164
}
164165

165-
func mergeDeployments(gcc v1alpha1.GatewayClassConfig, a, b *appsv1.Deployment) *appsv1.Deployment {
166+
func mergeDeployments(log logr.Logger, gcc v1alpha1.GatewayClassConfig, gateway gwv1beta1.Gateway, a, b *appsv1.Deployment) *appsv1.Deployment {
166167
if !compareDeployments(a, b) {
168+
// Replace template
167169
b.Spec.Template = a.Spec.Template
168170
b.Spec.Replicas = deploymentReplicas(gcc, a.Spec.Replicas)
169171
}
170172

173+
// Apply probes from Gateway annotations if present
174+
probes, err := ProbesFromGateway(&gateway)
175+
if err != nil {
176+
log.Error(err, "failed to parse probe annotations, skipping probe configuration")
177+
} else if probes != nil {
178+
for i, c := range b.Spec.Template.Spec.Containers {
179+
if i > 0 { // only primary container gets managed probes
180+
continue
181+
}
182+
if probes.Liveness != nil {
183+
c.LivenessProbe = probes.Liveness.DeepCopy()
184+
}
185+
if probes.Readiness != nil {
186+
c.ReadinessProbe = probes.Readiness.DeepCopy()
187+
}
188+
if probes.Startup != nil {
189+
c.StartupProbe = probes.Startup.DeepCopy()
190+
}
191+
b.Spec.Template.Spec.Containers[i] = c
192+
}
193+
}
194+
171195
return b
172196
}
173197

@@ -209,6 +233,27 @@ func compareDeployments(a, b *appsv1.Deployment) bool {
209233
return false
210234
}
211235
}
236+
237+
// Compare probe initialDelaySeconds for rollout restart functionality
238+
otherContainer := b.Spec.Template.Spec.Containers[i]
239+
240+
// Compare readiness probe initialDelaySeconds
241+
if container.ReadinessProbe != nil && otherContainer.ReadinessProbe != nil {
242+
if container.ReadinessProbe.InitialDelaySeconds != otherContainer.ReadinessProbe.InitialDelaySeconds {
243+
return false
244+
}
245+
} else if (container.ReadinessProbe == nil) != (otherContainer.ReadinessProbe == nil) {
246+
return false
247+
}
248+
249+
// Compare startup probe initialDelaySeconds
250+
if container.StartupProbe != nil && otherContainer.StartupProbe != nil {
251+
if container.StartupProbe.InitialDelaySeconds != otherContainer.StartupProbe.InitialDelaySeconds {
252+
return false
253+
}
254+
} else if (container.StartupProbe == nil) != (otherContainer.StartupProbe == nil) {
255+
return false
256+
}
212257
}
213258

214259
if b.Spec.Replicas == nil && a.Spec.Replicas == nil {
@@ -234,9 +279,9 @@ func mergeAnnotation(b *appsv1.Deployment, annotations map[string]string) {
234279

235280
}
236281

237-
func newDeploymentMutator(deployment, mutated, existingDeployment *appsv1.Deployment, deploymentExists bool, gcc v1alpha1.GatewayClassConfig, gateway gwv1beta1.Gateway, scheme *runtime.Scheme) resourceMutator {
282+
func newDeploymentMutator(deployment, mutated, existingDeployment *appsv1.Deployment, deploymentExists bool, gcc v1alpha1.GatewayClassConfig, gateway gwv1beta1.Gateway, scheme *runtime.Scheme, log logr.Logger) resourceMutator {
238283
return func() error {
239-
mutated = mergeDeployments(gcc, deployment, mutated)
284+
mutated = mergeDeployments(log, gcc, gateway, deployment, mutated)
240285
if deploymentExists {
241286
mergeAnnotation(mutated, existingDeployment.Spec.Template.Annotations)
242287
}

control-plane/api-gateway/gatekeeper/deployment_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@ package gatekeeper
66
import (
77
"testing"
88

9+
"github.com/go-logr/logr"
910
"github.com/stretchr/testify/assert"
1011
appsv1 "k8s.io/api/apps/v1"
1112
corev1 "k8s.io/api/core/v1"
13+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
gwv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
1215

1316
"github.com/hashicorp/consul-k8s/control-plane/api-gateway/common"
17+
"github.com/hashicorp/consul-k8s/control-plane/api/v1alpha1"
1418
)
1519

1620
func Test_compareDeployments(t *testing.T) {
@@ -217,3 +221,74 @@ func Test_compareDeployments(t *testing.T) {
217221
})
218222
}
219223
}
224+
225+
func TestMergeDeployments_ProbePropagation(t *testing.T) {
226+
t.Parallel()
227+
228+
log := logr.Discard()
229+
230+
gcc := v1alpha1.GatewayClassConfig{}
231+
232+
gateway := gwv1beta1.Gateway{
233+
ObjectMeta: metav1.ObjectMeta{
234+
Name: "test-gateway",
235+
Annotations: map[string]string{
236+
AnnotationLivenessProbe: `{
237+
"httpGet": {
238+
"path": "/health",
239+
"port": 8080
240+
}
241+
}`,
242+
},
243+
},
244+
}
245+
246+
deployment := &appsv1.Deployment{
247+
Spec: appsv1.DeploymentSpec{
248+
Template: corev1.PodTemplateSpec{
249+
Spec: corev1.PodSpec{
250+
Containers: []corev1.Container{
251+
{
252+
Name: "consul-dataplane",
253+
},
254+
},
255+
},
256+
},
257+
},
258+
}
259+
260+
merged := mergeDeployments(log, gcc, gateway, deployment, &appsv1.Deployment{})
261+
assert.NotNil(t, merged)
262+
263+
// Verify probe was applied to the container
264+
assert.Len(t, merged.Spec.Template.Spec.Containers, 1)
265+
container := merged.Spec.Template.Spec.Containers[0]
266+
assert.NotNil(t, container.LivenessProbe)
267+
assert.NotNil(t, container.LivenessProbe.HTTPGet)
268+
assert.Equal(t, "/health", container.LivenessProbe.HTTPGet.Path)
269+
assert.Equal(t, int32(8080), container.LivenessProbe.HTTPGet.Port.IntVal)
270+
271+
// Now update Gateway with different probe (TCPSocket instead of HTTPGet)
272+
gatewayUpdated := gwv1beta1.Gateway{
273+
ObjectMeta: metav1.ObjectMeta{
274+
Name: "test-gateway",
275+
Annotations: map[string]string{
276+
AnnotationLivenessProbe: `{
277+
"tcpSocket": {
278+
"port": 9090
279+
}
280+
}`,
281+
},
282+
},
283+
}
284+
285+
mergedUpdated := mergeDeployments(log, gcc, gatewayUpdated, merged, &appsv1.Deployment{})
286+
assert.NotNil(t, mergedUpdated)
287+
288+
// Verify the probe handler was replaced (TCPSocket instead of HTTPGet)
289+
containerUpdated := mergedUpdated.Spec.Template.Spec.Containers[0]
290+
assert.NotNil(t, containerUpdated.LivenessProbe)
291+
assert.NotNil(t, containerUpdated.LivenessProbe.TCPSocket)
292+
assert.Nil(t, containerUpdated.LivenessProbe.HTTPGet)
293+
assert.Equal(t, int32(9090), containerUpdated.LivenessProbe.TCPSocket.Port.IntVal)
294+
}

0 commit comments

Comments
 (0)