Merge pull request #89547 from smarterclayton/graceful

netexec: Allow graceful shutdown testing from netexec
This commit is contained in:
Kubernetes Prow Robot 2020-05-09 16:49:50 -07:00 committed by GitHub
commit d63d77dc4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 72 additions and 22 deletions

View File

@ -392,8 +392,14 @@ Starts a HTTP(S) server on given port with the following endpoints:
Acceptable values: `http`, `udp`, `sctp`. Acceptable values: `http`, `udp`, `sctp`.
- `tries`: The number of times the request will be performed. Default value: `1`. - `tries`: The number of times the request will be performed. Default value: `1`.
- `/echo`: Returns the given `msg` (`/echo?msg=echoed_msg`) - `/echo`: Returns the given `msg` (`/echo?msg=echoed_msg`)
- `/exit`: Closes the server with the given code (`/exit?code=some-code`). The `code` - `/exit`: Closes the server with the given code and graceful shutdown. The endpoint's parameters
is expected to be an integer [0-127] or empty; if it is not, it will return an error message. are:
- `code`: The exit code for the process. Default value: 0. Allows an integer [0-127].
- `timeout`: The amount of time to wait for connections to close before shutting down.
Acceptable values are golang durations. If 0 the process will exit immediately without
shutdown.
- `wait`: The amount of time to wait before starting shutdown. Acceptable values are
golang durations. If 0 the process will start shutdown immediately.
- `/healthz`: Returns `200 OK` if the server is ready, `412 Status Precondition Failed` - `/healthz`: Returns `200 OK` if the server is ready, `412 Status Precondition Failed`
otherwise. The server is considered not ready if the UDP server did not start yet or otherwise. The server is considered not ready if the UDP server did not start yet or
it exited. it exited.

View File

@ -1 +1 @@
2.15 2.16

View File

@ -49,7 +49,7 @@ import (
) )
func main() { func main() {
rootCmd := &cobra.Command{Use: "app", Version: "2.15"} rootCmd := &cobra.Command{Use: "app", Version: "2.16"}
rootCmd.AddCommand(auditproxy.CmdAuditProxy) rootCmd.AddCommand(auditproxy.CmdAuditProxy)
rootCmd.AddCommand(connect.CmdConnect) rootCmd.AddCommand(connect.CmdConnect)

View File

@ -17,6 +17,7 @@ limitations under the License.
package netexec package netexec
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@ -69,8 +70,14 @@ var CmdNetexec = &cobra.Command{
Acceptable values: "http", "udp", "sctp". Acceptable values: "http", "udp", "sctp".
- "tries": The number of times the request will be performed. Default value: "1". - "tries": The number of times the request will be performed. Default value: "1".
- "/echo": Returns the given "msg" ("/echo?msg=echoed_msg") - "/echo": Returns the given "msg" ("/echo?msg=echoed_msg")
- "/exit": Closes the server with the given code ("/exit?code=some-code"). The "code" - "/exit": Closes the server with the given code and graceful shutdown. The endpoint's parameters
is expected to be an integer [0-127] or empty; if it is not, it will return an error message. are:
- "code": The exit code for the process. Default value: 0. Allows an integer [0-127].
- "timeout": The amount of time to wait for connections to close before shutting down.
Acceptable values are golang durations. If 0 the process will exit immediately without
shutdown.
- "wait": The amount of time to wait before starting shutdown. Acceptable values are
golang durations. If 0 the process will start shutdown immediately.
- "/healthz": Returns "200 OK" if the server is ready, "412 Status Precondition Failed" - "/healthz": Returns "200 OK" if the server is ready, "412 Status Precondition Failed"
otherwise. The server is considered not ready if the UDP server did not start yet or otherwise. The server is considered not ready if the UDP server did not start yet or
it exited. it exited.
@ -127,25 +134,27 @@ func (a *atomicBool) get() bool {
} }
func main(cmd *cobra.Command, args []string) { func main(cmd *cobra.Command, args []string) {
exitCh := make(chan shutdownRequest)
addRoutes(exitCh)
go startUDPServer(udpPort) go startUDPServer(udpPort)
if sctpPort != -1 { if sctpPort != -1 {
go startSCTPServer(sctpPort) go startSCTPServer(sctpPort)
} }
addRoutes() server := &http.Server{Addr: fmt.Sprintf(":%d", httpPort)}
if len(certFile) > 0 { if len(certFile) > 0 {
// only start HTTPS server if a cert is provided startServer(server, exitCh, func() error { return server.ListenAndServeTLS(certFile, privKeyFile) })
startHTTPSServer(httpPort, certFile, privKeyFile)
} else { } else {
startHTTPServer(httpPort) startServer(server, exitCh, server.ListenAndServe)
} }
} }
func addRoutes() { func addRoutes(exitCh chan shutdownRequest) {
http.HandleFunc("/", rootHandler) http.HandleFunc("/", rootHandler)
http.HandleFunc("/clientip", clientIPHandler) http.HandleFunc("/clientip", clientIPHandler)
http.HandleFunc("/echo", echoHandler) http.HandleFunc("/echo", echoHandler)
http.HandleFunc("/exit", exitHandler) http.HandleFunc("/exit", func(w http.ResponseWriter, req *http.Request) { exitHandler(w, req, exitCh) })
http.HandleFunc("/hostname", hostnameHandler) http.HandleFunc("/hostname", hostnameHandler)
http.HandleFunc("/shell", shellHandler) http.HandleFunc("/shell", shellHandler)
http.HandleFunc("/upload", uploadHandler) http.HandleFunc("/upload", uploadHandler)
@ -156,12 +165,23 @@ func addRoutes() {
http.HandleFunc("/shutdown", shutdownHandler) http.HandleFunc("/shutdown", shutdownHandler)
} }
func startHTTPSServer(httpsPort int, certFile, privKeyFile string) { func startServer(server *http.Server, exitCh chan shutdownRequest, fn func() error) {
log.Fatal(http.ListenAndServeTLS(fmt.Sprintf(":%d", httpPort), certFile, privKeyFile, nil)) go func() {
} re := <-exitCh
ctx, cancelFn := context.WithTimeout(context.Background(), re.timeout)
defer cancelFn()
err := server.Shutdown(ctx)
log.Printf("Graceful shutdown completed with: %v", err)
os.Exit(re.code)
}()
func startHTTPServer(httpPort int) { if err := fn(); err != nil {
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", httpPort), nil)) if err == http.ErrServerClosed {
// wait until the goroutine calls os.Exit()
select {}
}
log.Fatal(err)
}
} }
func rootHandler(w http.ResponseWriter, r *http.Request) { func rootHandler(w http.ResponseWriter, r *http.Request) {
@ -179,13 +199,37 @@ func clientIPHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, r.RemoteAddr) fmt.Fprintf(w, r.RemoteAddr)
} }
func exitHandler(w http.ResponseWriter, r *http.Request) { type shutdownRequest struct {
log.Printf("GET /exit?code=%s", r.FormValue("code")) code int
code, err := strconv.Atoi(r.FormValue("code")) timeout time.Duration
if err == nil || r.FormValue("code") == "" { }
func exitHandler(w http.ResponseWriter, r *http.Request, exitCh chan<- shutdownRequest) {
waitString := r.FormValue("wait")
timeoutString := r.FormValue("timeout")
codeString := r.FormValue("code")
log.Printf("GET /exit?code=%s&timeout=%s&wait=%s", codeString, timeoutString, waitString)
timeout, err := time.ParseDuration(timeoutString)
if err != nil && timeoutString != "" {
fmt.Fprintf(w, "argument 'timeout' must be a valid golang duration or empty, got %q\n", timeoutString)
return
}
wait, err := time.ParseDuration(waitString)
if err != nil && waitString != "" {
fmt.Fprintf(w, "argument 'wait' must be a valid golang duration or empty, got %q\n", waitString)
return
}
code, err := strconv.Atoi(codeString)
if err != nil && codeString != "" {
fmt.Fprintf(w, "argument 'code' must be an integer [0-127] or empty, got %q\n", codeString)
return
}
log.Printf("Will begin shutdown in %s, allowing %s for connections to close, then will exit with %d", wait, timeout, code)
time.Sleep(wait)
if timeout == 0 {
os.Exit(code) os.Exit(code)
} }
fmt.Fprintf(w, "argument 'code' must be an integer [0-127] or empty, got %q", r.FormValue("code")) exitCh <- shutdownRequest{code: code, timeout: timeout}
} }
func hostnameHandler(w http.ResponseWriter, r *http.Request) { func hostnameHandler(w http.ResponseWriter, r *http.Request) {