Skip to content

Commit f6747f9

Browse files
authored
feat: Add shutdown command to call the /quitquitquit endpoint. (#2514)
This adds the `shutdown` command the proxy, which will cause a proxy running on the same host to shut down. It invokes the shutdown command by sending an HTTP Post to `http://127.0.0.1:9101/quitquitquit`. This is especially useful as a preStop hook in Kubernetes. Run the proxy container with the --quitquitquit and --admin-port flags. Then configure the pre-stop hook with the same admin port. ``` # Configure kubernetes to call run the cloud-sql-proxy shutdown command # before sending SIGTERM to the proxy when stopping the pod. This will # give the proxy more time to gracefully exit. lifecycle: preStop: exec: command: ["/cloud-sql-proxy","shutdown", "--admin-port","<ADMIN_PORT>"] ``` Fixes #2511
1 parent eb04fe4 commit f6747f9

File tree

6 files changed

+193
-10
lines changed

6 files changed

+193
-10
lines changed

build.sh

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ function clean() {
3333

3434
## build - Builds the project without running tests.
3535
function build() {
36-
go build ./...
36+
go build -o ./cloud-sql-proxy main.go
3737
}
3838

3939
## test - Runs local unit tests.
4040
function test() {
41-
go test -v -race -cover -short
41+
go test -v -race -cover -short ./...
4242
}
4343

4444
## e2e - Runs end-to-end integration tests.
@@ -83,6 +83,9 @@ function fix() {
8383
".tools/goimports" -w .
8484
go mod tidy
8585
go fmt ./...
86+
87+
# Generate CMD docs
88+
go run ./cmd/gendocs/gen_cloud-sql-proxy_docs.go
8689
}
8790

8891
## lint - runs the linters

cmd/root.go

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,8 +349,33 @@ Configuration
349349
./cloud-sql-proxy wait --max 10s
350350
`
351351

352+
var shutdownHelp = `
353+
Shutting Down the Proxy
354+
355+
The shutdown command signals a running Proxy process to gracefully shut
356+
down. This is useful for scripting and for Kubernetes environments.
357+
358+
The shutdown command requires that the Proxy be started in another process
359+
with the admin server enabled. For example:
360+
361+
./cloud-sql-proxy <INSTANCE_CONNECTION_NAME> --quitquitquit
362+
363+
Invoke the shutdown command like this:
364+
365+
# signals another Proxy process to shut down
366+
./cloud-sql-proxy shutdown
367+
368+
Configuration
369+
370+
If the running Proxy is configured with a non-default admin port, the
371+
shutdown command must also be told to use the same custom value:
372+
373+
./cloud-sql-proxy shutdown --admin-port 9192
374+
`
375+
352376
const (
353377
waitMaxFlag = "max"
378+
adminPortFlag = "admin-port"
354379
httpAddressFlag = "http-address"
355380
httpPortFlag = "http-port"
356381
)
@@ -384,6 +409,29 @@ func runWaitCmd(c *cobra.Command, _ []string) error {
384409
}
385410
}
386411

412+
func runShutdownCmd(c *cobra.Command, _ []string) error {
413+
p, _ := c.Flags().GetString(adminPortFlag)
414+
addr := fmt.Sprintf("http://127.0.0.1:%v/quitquitquit", p)
415+
c.SilenceUsage = true
416+
417+
req, err := http.NewRequestWithContext(c.Context(), "POST", addr, nil)
418+
if err != nil {
419+
return fmt.Errorf("failed to create shutdown request: %w", err)
420+
}
421+
422+
resp, err := http.DefaultClient.Do(req)
423+
if err != nil {
424+
return fmt.Errorf("failed to send shutdown request: %w", err)
425+
}
426+
defer resp.Body.Close()
427+
428+
if resp.StatusCode != http.StatusOK {
429+
return fmt.Errorf("shutdown request failed: status code %v, %v", resp.StatusCode, resp.Status)
430+
}
431+
432+
return nil
433+
}
434+
387435
const envPrefix = "CSQL_PROXY"
388436

389437
// NewCommand returns a Command object representing an invocation of the proxy.
@@ -419,6 +467,20 @@ func NewCommand(opts ...Option) *Command {
419467
)
420468
rootCmd.AddCommand(waitCmd)
421469

470+
var shutdownCmd = &cobra.Command{
471+
Use: "shutdown",
472+
Short: "Signal a running Proxy process to shut down",
473+
Long: shutdownHelp,
474+
RunE: runShutdownCmd,
475+
}
476+
shutdownFlags := shutdownCmd.Flags()
477+
shutdownFlags.String(
478+
adminPortFlag,
479+
"9091",
480+
"port for the admin server",
481+
)
482+
rootCmd.AddCommand(shutdownCmd)
483+
422484
rootCmd.Args = func(_ *cobra.Command, args []string) error {
423485
// Load the configuration file before running the command. This should
424486
// ensure that the configuration is loaded in the correct order:
@@ -490,7 +552,7 @@ the Proxy will then pick-up automatically.`)
490552
"Enable pprof on the localhost admin server")
491553
localFlags.BoolVar(&c.conf.QuitQuitQuit, "quitquitquit", false,
492554
"Enable quitquitquit endpoint on the localhost admin server")
493-
localFlags.StringVar(&c.conf.AdminPort, "admin-port", "9091",
555+
localFlags.StringVar(&c.conf.AdminPort, adminPortFlag, "9091",
494556
"Port for localhost-only admin server")
495557
localFlags.BoolVar(&c.conf.HealthCheck, "health-check", false,
496558
"Enables health check endpoints /startup, /liveness, and /readiness on localhost.")

cmd/shutdown_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cmd
16+
17+
import (
18+
"net"
19+
"net/http"
20+
"net/http/httptest"
21+
"testing"
22+
"time"
23+
)
24+
25+
func TestShutdownCommand(t *testing.T) {
26+
shutdownCh := make(chan bool, 1)
27+
handler := func(w http.ResponseWriter, r *http.Request) {
28+
if r.Method != "POST" {
29+
t.Errorf("want = POST, got = %v", r.Method)
30+
}
31+
w.WriteHeader(http.StatusOK)
32+
shutdownCh <- true
33+
}
34+
server := httptest.NewServer(http.HandlerFunc(handler))
35+
defer server.Close()
36+
37+
_, port, err := net.SplitHostPort(server.Listener.Addr().String())
38+
if err != nil {
39+
t.Fatal(err)
40+
}
41+
42+
_, err = invokeProxyCommand([]string{
43+
"shutdown",
44+
"--admin-port", port,
45+
})
46+
if err != nil {
47+
t.Fatalf("invokeProxyCommand failed: %v", err)
48+
}
49+
50+
select {
51+
case <-shutdownCh:
52+
// success
53+
case <-time.After(1 * time.Second):
54+
t.Fatal("server did not receive shutdown request")
55+
}
56+
}
57+
58+
func TestShutdownCommandFails(t *testing.T) {
59+
_, err := invokeProxyCommand([]string{
60+
"shutdown",
61+
// assuming default host and port
62+
"--wait=100ms",
63+
})
64+
if err == nil {
65+
t.Fatal("shutdown should fail when endpoint does not respond")
66+
}
67+
}

docs/cmd/cloud-sql-proxy.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,5 +294,6 @@ cloud-sql-proxy INSTANCE_CONNECTION_NAME... [flags]
294294
### SEE ALSO
295295

296296
* [cloud-sql-proxy completion](cloud-sql-proxy_completion.md) - Generate the autocompletion script for the specified shell
297+
* [cloud-sql-proxy shutdown](cloud-sql-proxy_shutdown.md) - Signal a running Proxy process to shut down
297298
* [cloud-sql-proxy wait](cloud-sql-proxy_wait.md) - Wait for another Proxy process to start
298299

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
## cloud-sql-proxy shutdown
2+
3+
Signal a running Proxy process to shut down
4+
5+
### Synopsis
6+
7+
8+
Shutting Down the Proxy
9+
10+
The shutdown command signals a running Proxy process to gracefully shut
11+
down. This is useful for scripting and for Kubernetes environments.
12+
13+
The shutdown command requires that the Proxy be started in another process
14+
with the admin server enabled. For example:
15+
16+
./cloud-sql-proxy <INSTANCE_CONNECTION_NAME> --quitquitquit
17+
18+
Invoke the shutdown command like this:
19+
20+
# signals another Proxy process to shut down
21+
./cloud-sql-proxy shutdown
22+
23+
Configuration
24+
25+
If the running Proxy is configured with a non-default admin port, the
26+
shutdown command must also be told to use the same custom value:
27+
28+
./cloud-sql-proxy shutdown --admin-port 9192
29+
30+
31+
```
32+
cloud-sql-proxy shutdown [flags]
33+
```
34+
35+
### Options
36+
37+
```
38+
--admin-port string port for the admin server (default "9091")
39+
-h, --help help for shutdown
40+
```
41+
42+
### Options inherited from parent commands
43+
44+
```
45+
--http-address string Address for Prometheus and health check server (default "localhost")
46+
--http-port string Port for Prometheus and health check server (default "9090")
47+
```
48+
49+
### SEE ALSO
50+
51+
* [cloud-sql-proxy](cloud-sql-proxy.md) - cloud-sql-proxy authorizes and encrypts connections to Cloud SQL.
52+

examples/k8s-health-check/proxy_with_http_health_check.yaml

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -112,15 +112,13 @@ spec:
112112
- name: CSQL_PROXY_STRUCTURED_LOGS
113113
value: "true"
114114

115-
# Configure kubernetes to call the /quitquitquit endpoint on the
116-
# admin server before sending SIGTERM to the proxy before stopping
117-
# the pod. This will give the proxy more time to gracefully exit.
115+
# Configure kubernetes to call run the cloud-sql-proxy shutdown command
116+
# before sending SIGTERM to the proxy when stopping the pod. This will
117+
# give the proxy more time to gracefully exit.
118118
lifecycle:
119119
preStop:
120-
httpGet:
121-
path: /quitquitquit
122-
port: 9092
123-
scheme: HTTP
120+
exec:
121+
command: ["/cloud-sql-proxy","shutdown", "--admin-port","9192"]
124122

125123
# The /startup probe returns OK when the proxy is ready to receive
126124
# connections from the application. In this example, k8s will check

0 commit comments

Comments
 (0)