Skip to content

Commit 1aee555

Browse files
committed
Add support for startup probes
1 parent 8475c55 commit 1aee555

File tree

8 files changed

+122
-6
lines changed

8 files changed

+122
-6
lines changed

designs/component-config.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# ComponentConfig Controller Runtime Support
22
Author: @christopherhein
33

4-
Last Updated on: 03/02/2020
4+
Last Updated on: 31/01/2024
55

66
## Table of Contents
77

@@ -91,6 +91,7 @@ type ManagerConfiguration interface {
9191

9292
GetReadinessEndpointName() string
9393
GetLivenessEndpointName() string
94+
GetStartupEndpointName() string
9495

9596
GetPort() int
9697
GetHost() string
@@ -161,6 +162,7 @@ type ControllerManagerConfigurationHealth struct {
161162

162163
ReadinessEndpointName string `json:"readinessEndpointName,omitempty"`
163164
LivenessEndpointName string `json:"livenessEndpointName,omitempty"`
165+
StartupEndpointName string `json:"startupEndpointName,omitempty"`
164166
}
165167
```
166168

designs/move-cluster-specific-code-out-of-manager.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ type Manager interface {
116116
// AddReadyzCheck allows you to add Readyz checker
117117
AddReadyzCheck(name string, check healthz.Checker) error
118118

119+
// AddStartzCheck allows you to add Startz checker
120+
AddStartzCheck(name string, check healthz.Checker) error
121+
119122
// Start starts all registered Controllers and blocks until the Stop channel is closed.
120123
// Returns an error if there is an error starting any controller.
121124
// If LeaderElection is used, the binary must be exited immediately after this returns,

pkg/config/v1alpha1/types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ type ControllerHealth struct {
135135
// LivenessEndpointName, defaults to "healthz"
136136
// +optional
137137
LivenessEndpointName string `json:"livenessEndpointName,omitempty"`
138+
139+
// StartupEndpointName, defaults to "startz"
140+
// +optional
141+
StartupEndpointName string `json:"startupEndpointName,omitempty"`
138142
}
139143

140144
// ControllerWebhook defines the webhook server for the controller.

pkg/healthz/doc.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
// Package healthz contains helpers from supporting liveness and readiness endpoints.
18-
// (often referred to as healthz and readyz, respectively).
17+
// Package healthz contains helpers from supporting liveness, readiness and startup endpoints.
18+
// (often referred to as healthz, readyz and startz, respectively).
1919
//
2020
// This package draws heavily from the apiserver's healthz package
2121
// ( https://github.com/kubernetes/apiserver/tree/master/pkg/server/healthz )

pkg/manager/internal.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const (
5656

5757
defaultReadinessEndpoint = "/readyz"
5858
defaultLivenessEndpoint = "/healthz"
59+
defaultStartupEndpoint = "/startz"
5960
)
6061

6162
var _ Runnable = &controllerManager{}
@@ -94,12 +95,18 @@ type controllerManager struct {
9495
// Liveness probe endpoint name
9596
livenessEndpointName string
9697

98+
// Startup probe endpoint name
99+
startupEndpointName string
100+
97101
// Readyz probe handler
98102
readyzHandler *healthz.Handler
99103

100104
// Healthz probe handler
101105
healthzHandler *healthz.Handler
102106

107+
// Startz probe handler
108+
startzHandler *healthz.Handler
109+
103110
// pprofListener is used to serve pprof
104111
pprofListener net.Listener
105112

@@ -213,6 +220,23 @@ func (cm *controllerManager) AddReadyzCheck(name string, check healthz.Checker)
213220
return nil
214221
}
215222

223+
// AddStartzCheck allows you to add Startz checker.
224+
func (cm *controllerManager) AddStartzCheck(name string, check healthz.Checker) error {
225+
cm.Lock()
226+
defer cm.Unlock()
227+
228+
if cm.started {
229+
return fmt.Errorf("unable to add new checker because healthz endpoint has already been created")
230+
}
231+
232+
if cm.startzHandler == nil {
233+
cm.startzHandler = &healthz.Handler{Checks: map[string]healthz.Checker{}}
234+
}
235+
236+
cm.startzHandler.Checks[name] = check
237+
return nil
238+
}
239+
216240
func (cm *controllerManager) GetHTTPClient() *http.Client {
217241
return cm.cluster.GetHTTPClient()
218242
}
@@ -283,6 +307,11 @@ func (cm *controllerManager) addHealthProbeServer() error {
283307
// Append '/' suffix to handle subpaths
284308
mux.Handle(cm.livenessEndpointName+"/", http.StripPrefix(cm.livenessEndpointName, cm.healthzHandler))
285309
}
310+
if cm.startzHandler != nil {
311+
mux.Handle(cm.startupEndpointName, http.StripPrefix(cm.startupEndpointName, cm.startzHandler))
312+
// Append '/' suffix to handle subpaths
313+
mux.Handle(cm.startupEndpointName+"/", http.StripPrefix(cm.startupEndpointName, cm.startzHandler))
314+
}
286315

287316
return cm.add(&server{
288317
Kind: "health probe",

pkg/manager/internal/integration/manager_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ var _ = Describe("manger.Manager Start", func() {
158158
// Configure health probes.
159159
Expect(mgr.AddReadyzCheck("webhook", mgr.GetWebhookServer().StartedChecker())).To(Succeed())
160160
Expect(mgr.AddHealthzCheck("webhook", mgr.GetWebhookServer().StartedChecker())).To(Succeed())
161+
Expect(mgr.AddStartzCheck("webhook", mgr.GetWebhookServer().StartedChecker())).To(Succeed())
161162

162163
// Set up Driver reconciler (using v2).
163164
driverReconciler := &DriverReconciler{

pkg/manager/manager.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ type Manager interface {
7373
// AddReadyzCheck allows you to add Readyz checker
7474
AddReadyzCheck(name string, check healthz.Checker) error
7575

76+
// AddStartzCheck allows you to add Startz checker
77+
AddStartzCheck(name string, check healthz.Checker) error
78+
7679
// Start starts all registered Controllers and blocks until the context is cancelled.
7780
// Returns an error if there is an error starting any controller.
7881
//
@@ -228,6 +231,9 @@ type Options struct {
228231
// Liveness probe endpoint name, defaults to "healthz"
229232
LivenessEndpointName string
230233

234+
// Startup probe endpoint name, defaults to "healthz"
235+
StartupEndpointName string
236+
231237
// PprofBindAddress is the TCP address that the controller should bind to
232238
// for serving pprof.
233239
// It can be set to "" or "0" to disable the pprof serving.
@@ -430,6 +436,7 @@ func New(config *rest.Config, options Options) (Manager, error) {
430436
healthProbeListener: healthProbeListener,
431437
readinessEndpointName: options.ReadinessEndpointName,
432438
livenessEndpointName: options.LivenessEndpointName,
439+
startupEndpointName: options.StartupEndpointName,
433440
pprofListener: pprofListener,
434441
gracefulShutdownTimeout: *options.GracefulShutdownTimeout,
435442
internalProceduresStop: make(chan struct{}),
@@ -480,6 +487,10 @@ func (o Options) AndFrom(loader config.ControllerManagerConfiguration) (Options,
480487
o.LivenessEndpointName = newObj.Health.LivenessEndpointName
481488
}
482489

490+
if o.StartupEndpointName == "" && newObj.Health.StartupEndpointName != "" {
491+
o.StartupEndpointName = newObj.Health.StartupEndpointName
492+
}
493+
483494
if o.WebhookServer == nil {
484495
port := 0
485496
if newObj.Webhook.Port != nil {
@@ -640,6 +651,10 @@ func setOptionsDefaults(options Options) Options {
640651
options.LivenessEndpointName = defaultLivenessEndpoint
641652
}
642653

654+
if options.StartupEndpointName == "" {
655+
options.StartupEndpointName = defaultStartupEndpoint
656+
}
657+
643658
if options.newHealthProbeListener == nil {
644659
options.newHealthProbeListener = defaultHealthProbeListener
645660
}

pkg/manager/manager_test.go

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ var _ = Describe("manger.Manager", func() {
145145
HealthProbeBindAddress: "6060",
146146
ReadinessEndpointName: "/readyz",
147147
LivenessEndpointName: "/livez",
148+
StartupEndpointName: "/startz",
148149
},
149150
Webhook: v1alpha1.ControllerWebhook{
150151
Port: &port,
@@ -170,6 +171,7 @@ var _ = Describe("manger.Manager", func() {
170171
Expect(m.HealthProbeBindAddress).To(Equal("6060"))
171172
Expect(m.ReadinessEndpointName).To(Equal("/readyz"))
172173
Expect(m.LivenessEndpointName).To(Equal("/livez"))
174+
Expect(m.StartupEndpointName).To(Equal("/startz"))
173175
Expect(m.WebhookServer.(*webhook.DefaultServer).Options.Port).To(Equal(port))
174176
Expect(m.WebhookServer.(*webhook.DefaultServer).Options.Host).To(Equal("localhost"))
175177
Expect(m.WebhookServer.(*webhook.DefaultServer).Options.CertDir).To(Equal("/certs"))
@@ -201,6 +203,7 @@ var _ = Describe("manger.Manager", func() {
201203
HealthProbeBindAddress: "6060",
202204
ReadinessEndpointName: "/readyz",
203205
LivenessEndpointName: "/livez",
206+
StartupEndpointName: "/startz",
204207
},
205208
Webhook: v1alpha1.ControllerWebhook{
206209
Port: &port,
@@ -229,6 +232,7 @@ var _ = Describe("manger.Manager", func() {
229232
HealthProbeBindAddress: "5000",
230233
ReadinessEndpointName: "/readiness",
231234
LivenessEndpointName: "/liveness",
235+
StartupEndpointName: "/startup",
232236
WebhookServer: webhook.NewServer(webhook.Options{
233237
Port: 8080,
234238
Host: "example.com",
@@ -251,6 +255,7 @@ var _ = Describe("manger.Manager", func() {
251255
Expect(m.HealthProbeBindAddress).To(Equal("5000"))
252256
Expect(m.ReadinessEndpointName).To(Equal("/readiness"))
253257
Expect(m.LivenessEndpointName).To(Equal("/liveness"))
258+
Expect(m.StartupEndpointName).To(Equal("/startup"))
254259
Expect(m.WebhookServer.(*webhook.DefaultServer).Options.Port).To(Equal(8080))
255260
Expect(m.WebhookServer.(*webhook.DefaultServer).Options.Host).To(Equal("example.com"))
256261
Expect(m.WebhookServer.(*webhook.DefaultServer).Options.CertDir).To(Equal("/pki"))
@@ -1399,6 +1404,11 @@ var _ = Describe("manger.Manager", func() {
13991404
})
14001405

14011406
Context("should start serving health probes", func() {
1407+
1408+
const (
1409+
namedCheck = "check"
1410+
)
1411+
14021412
var listener net.Listener
14031413
var opts Options
14041414

@@ -1452,7 +1462,6 @@ var _ = Describe("manger.Manager", func() {
14521462
Expect(err).NotTo(HaveOccurred())
14531463

14541464
res := fmt.Errorf("not ready yet")
1455-
namedCheck := "check"
14561465
err = m.AddReadyzCheck(namedCheck, func(_ *http.Request) error { return res })
14571466
Expect(err).NotTo(HaveOccurred())
14581467

@@ -1507,7 +1516,6 @@ var _ = Describe("manger.Manager", func() {
15071516
Expect(err).NotTo(HaveOccurred())
15081517

15091518
res := fmt.Errorf("not alive")
1510-
namedCheck := "check"
15111519
err = m.AddHealthzCheck(namedCheck, func(_ *http.Request) error { return res })
15121520
Expect(err).NotTo(HaveOccurred())
15131521

@@ -1547,14 +1555,68 @@ var _ = Describe("manger.Manager", func() {
15471555
defer resp.Body.Close()
15481556
Expect(resp.StatusCode).To(Equal(http.StatusOK))
15491557

1550-
// Check readiness path for individual check
1558+
// Check liveness path for individual check
15511559
livenessEndpoint = fmt.Sprint("http://", listener.Addr().String(), path.Join(defaultLivenessEndpoint, namedCheck))
15521560
res = nil
15531561
resp, err = http.Get(livenessEndpoint)
15541562
Expect(err).NotTo(HaveOccurred())
15551563
defer resp.Body.Close()
15561564
Expect(resp.StatusCode).To(Equal(http.StatusOK))
15571565
})
1566+
1567+
It("should serve startup endpoint", func() {
1568+
opts.HealthProbeBindAddress = ":0"
1569+
m, err := New(cfg, opts)
1570+
Expect(err).NotTo(HaveOccurred())
1571+
1572+
res := fmt.Errorf("not alive")
1573+
err = m.AddStartzCheck(namedCheck, func(_ *http.Request) error { return res })
1574+
Expect(err).NotTo(HaveOccurred())
1575+
1576+
ctx, cancel := context.WithCancel(context.Background())
1577+
defer cancel()
1578+
go func() {
1579+
defer GinkgoRecover()
1580+
Expect(m.Start(ctx)).NotTo(HaveOccurred())
1581+
}()
1582+
<-m.Elected()
1583+
1584+
startupEndpoint := fmt.Sprint("http://", listener.Addr().String(), defaultStartupEndpoint)
1585+
1586+
// Controller is not ready
1587+
resp, err := http.Get(startupEndpoint)
1588+
Expect(err).NotTo(HaveOccurred())
1589+
defer resp.Body.Close()
1590+
Expect(resp.StatusCode).To(Equal(http.StatusInternalServerError))
1591+
1592+
// Controller is ready
1593+
res = nil
1594+
resp, err = http.Get(startupEndpoint)
1595+
Expect(err).NotTo(HaveOccurred())
1596+
defer resp.Body.Close()
1597+
Expect(resp.StatusCode).To(Equal(http.StatusOK))
1598+
1599+
// Check startup path without trailing slash without redirect
1600+
startupEndpoint = fmt.Sprint("http://", listener.Addr().String(), defaultStartupEndpoint)
1601+
res = nil
1602+
httpClient := http.Client{
1603+
CheckRedirect: func(req *http.Request, via []*http.Request) error {
1604+
return http.ErrUseLastResponse // Do not follow redirect
1605+
},
1606+
}
1607+
resp, err = httpClient.Get(startupEndpoint)
1608+
Expect(err).NotTo(HaveOccurred())
1609+
defer resp.Body.Close()
1610+
Expect(resp.StatusCode).To(Equal(http.StatusOK))
1611+
1612+
// Check startup path for individual check
1613+
startupEndpoint = fmt.Sprint("http://", listener.Addr().String(), path.Join(defaultStartupEndpoint, namedCheck))
1614+
res = nil
1615+
resp, err = http.Get(startupEndpoint)
1616+
Expect(err).NotTo(HaveOccurred())
1617+
defer resp.Body.Close()
1618+
Expect(resp.StatusCode).To(Equal(http.StatusOK))
1619+
})
15581620
})
15591621

15601622
Context("should start serving pprof", func() {

0 commit comments

Comments
 (0)