diff --git a/build.sh b/build.sh index 34532205e..5cb9e2134 100755 --- a/build.sh +++ b/build.sh @@ -33,12 +33,12 @@ function clean() { ## build - Builds the project without running tests. function build() { - go build ./... + go build -o ./cloud-sql-proxy main.go } ## test - Runs local unit tests. function test() { - go test -v -race -cover -short + go test -v -race -cover -short ./... } ## e2e - Runs end-to-end integration tests. @@ -83,6 +83,9 @@ function fix() { ".tools/goimports" -w . go mod tidy go fmt ./... + + # Generate CMD docs + go run ./cmd/gendocs/gen_cloud-sql-proxy_docs.go } ## lint - runs the linters diff --git a/cmd/root.go b/cmd/root.go index 135e9fc6d..d8811390e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -349,8 +349,33 @@ Configuration ./cloud-sql-proxy wait --max 10s ` +var shutdownHelp = ` +Shutting Down the Proxy + + The shutdown command signals a running Proxy process to gracefully shut + down. This is useful for scripting and for Kubernetes environments. + + The shutdown command requires that the Proxy be started in another process + with the admin server enabled. For example: + + ./cloud-sql-proxy --quitquitquit + + Invoke the shutdown command like this: + + # signals another Proxy process to shut down + ./cloud-sql-proxy shutdown + +Configuration + + If the running Proxy is configured with a non-default admin port, the + shutdown command must also be told to use the same custom value: + + ./cloud-sql-proxy shutdown --admin-port 9192 +` + const ( waitMaxFlag = "max" + adminPortFlag = "admin-port" httpAddressFlag = "http-address" httpPortFlag = "http-port" ) @@ -384,6 +409,29 @@ func runWaitCmd(c *cobra.Command, _ []string) error { } } +func runShutdownCmd(c *cobra.Command, _ []string) error { + p, _ := c.Flags().GetString(adminPortFlag) + addr := fmt.Sprintf("http://127.0.0.1:%v/quitquitquit", p) + c.SilenceUsage = true + + req, err := http.NewRequestWithContext(c.Context(), "POST", addr, nil) + if err != nil { + return fmt.Errorf("failed to create shutdown request: %w", err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("failed to send shutdown request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("shutdown request failed: status code %v, %v", resp.StatusCode, resp.Status) + } + + return nil +} + const envPrefix = "CSQL_PROXY" // NewCommand returns a Command object representing an invocation of the proxy. @@ -419,6 +467,20 @@ func NewCommand(opts ...Option) *Command { ) rootCmd.AddCommand(waitCmd) + var shutdownCmd = &cobra.Command{ + Use: "shutdown", + Short: "Signal a running Proxy process to shut down", + Long: shutdownHelp, + RunE: runShutdownCmd, + } + shutdownFlags := shutdownCmd.Flags() + shutdownFlags.String( + adminPortFlag, + "9091", + "port for the admin server", + ) + rootCmd.AddCommand(shutdownCmd) + rootCmd.Args = func(_ *cobra.Command, args []string) error { // Load the configuration file before running the command. This should // ensure that the configuration is loaded in the correct order: @@ -490,7 +552,7 @@ the Proxy will then pick-up automatically.`) "Enable pprof on the localhost admin server") localFlags.BoolVar(&c.conf.QuitQuitQuit, "quitquitquit", false, "Enable quitquitquit endpoint on the localhost admin server") - localFlags.StringVar(&c.conf.AdminPort, "admin-port", "9091", + localFlags.StringVar(&c.conf.AdminPort, adminPortFlag, "9091", "Port for localhost-only admin server") localFlags.BoolVar(&c.conf.HealthCheck, "health-check", false, "Enables health check endpoints /startup, /liveness, and /readiness on localhost.") diff --git a/cmd/shutdown_test.go b/cmd/shutdown_test.go new file mode 100644 index 000000000..03bd44cad --- /dev/null +++ b/cmd/shutdown_test.go @@ -0,0 +1,67 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "net" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestShutdownCommand(t *testing.T) { + shutdownCh := make(chan bool, 1) + handler := func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Errorf("want = POST, got = %v", r.Method) + } + w.WriteHeader(http.StatusOK) + shutdownCh <- true + } + server := httptest.NewServer(http.HandlerFunc(handler)) + defer server.Close() + + _, port, err := net.SplitHostPort(server.Listener.Addr().String()) + if err != nil { + t.Fatal(err) + } + + _, err = invokeProxyCommand([]string{ + "shutdown", + "--admin-port", port, + }) + if err != nil { + t.Fatalf("invokeProxyCommand failed: %v", err) + } + + select { + case <-shutdownCh: + // success + case <-time.After(1 * time.Second): + t.Fatal("server did not receive shutdown request") + } +} + +func TestShutdownCommandFails(t *testing.T) { + _, err := invokeProxyCommand([]string{ + "shutdown", + // assuming default host and port + "--wait=100ms", + }) + if err == nil { + t.Fatal("shutdown should fail when endpoint does not respond") + } +} diff --git a/docs/cmd/cloud-sql-proxy.md b/docs/cmd/cloud-sql-proxy.md index 72f571d5b..19ed0c473 100644 --- a/docs/cmd/cloud-sql-proxy.md +++ b/docs/cmd/cloud-sql-proxy.md @@ -294,5 +294,6 @@ cloud-sql-proxy INSTANCE_CONNECTION_NAME... [flags] ### SEE ALSO * [cloud-sql-proxy completion](cloud-sql-proxy_completion.md) - Generate the autocompletion script for the specified shell +* [cloud-sql-proxy shutdown](cloud-sql-proxy_shutdown.md) - Signal a running Proxy process to shut down * [cloud-sql-proxy wait](cloud-sql-proxy_wait.md) - Wait for another Proxy process to start diff --git a/docs/cmd/cloud-sql-proxy_shutdown.md b/docs/cmd/cloud-sql-proxy_shutdown.md new file mode 100644 index 000000000..aa5befb24 --- /dev/null +++ b/docs/cmd/cloud-sql-proxy_shutdown.md @@ -0,0 +1,52 @@ +## cloud-sql-proxy shutdown + +Signal a running Proxy process to shut down + +### Synopsis + + +Shutting Down the Proxy + + The shutdown command signals a running Proxy process to gracefully shut + down. This is useful for scripting and for Kubernetes environments. + + The shutdown command requires that the Proxy be started in another process + with the admin server enabled. For example: + + ./cloud-sql-proxy --quitquitquit + + Invoke the shutdown command like this: + + # signals another Proxy process to shut down + ./cloud-sql-proxy shutdown + +Configuration + + If the running Proxy is configured with a non-default admin port, the + shutdown command must also be told to use the same custom value: + + ./cloud-sql-proxy shutdown --admin-port 9192 + + +``` +cloud-sql-proxy shutdown [flags] +``` + +### Options + +``` + --admin-port string port for the admin server (default "9091") + -h, --help help for shutdown +``` + +### Options inherited from parent commands + +``` + --http-address string Address for Prometheus and health check server (default "localhost") + --http-port string Port for Prometheus and health check server (default "9090") +``` + +### SEE ALSO + +* [cloud-sql-proxy](cloud-sql-proxy.md) - cloud-sql-proxy authorizes and encrypts connections to Cloud SQL. + diff --git a/examples/k8s-health-check/proxy_with_http_health_check.yaml b/examples/k8s-health-check/proxy_with_http_health_check.yaml index 8b1b6c03d..dc6b20f07 100644 --- a/examples/k8s-health-check/proxy_with_http_health_check.yaml +++ b/examples/k8s-health-check/proxy_with_http_health_check.yaml @@ -112,15 +112,13 @@ spec: - name: CSQL_PROXY_STRUCTURED_LOGS value: "true" - # Configure kubernetes to call the /quitquitquit endpoint on the - # admin server before sending SIGTERM to the proxy before stopping - # the pod. This will give the proxy more time to gracefully exit. + # 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: - httpGet: - path: /quitquitquit - port: 9092 - scheme: HTTP + exec: + command: ["/cloud-sql-proxy","shutdown", "--admin-port","9192"] # The /startup probe returns OK when the proxy is ready to receive # connections from the application. In this example, k8s will check