Skip to content

Commit d63d77d

Browse files
authored
Merge pull request kubernetes#89547 from smarterclayton/graceful
netexec: Allow graceful shutdown testing from netexec
2 parents 3446ffb + 3c9959a commit d63d77d

File tree

4 files changed

+72
-22
lines changed

4 files changed

+72
-22
lines changed

test/images/agnhost/README.md

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

test/images/agnhost/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.15
1+
2.16

test/images/agnhost/agnhost.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import (
4949
)
5050

5151
func main() {
52-
rootCmd := &cobra.Command{Use: "app", Version: "2.15"}
52+
rootCmd := &cobra.Command{Use: "app", Version: "2.16"}
5353

5454
rootCmd.AddCommand(auditproxy.CmdAuditProxy)
5555
rootCmd.AddCommand(connect.CmdConnect)

test/images/agnhost/netexec/netexec.go

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package netexec
1818

1919
import (
20+
"context"
2021
"encoding/json"
2122
"fmt"
2223
"io"
@@ -69,8 +70,14 @@ var CmdNetexec = &cobra.Command{
6970
Acceptable values: "http", "udp", "sctp".
7071
- "tries": The number of times the request will be performed. Default value: "1".
7172
- "/echo": Returns the given "msg" ("/echo?msg=echoed_msg")
72-
- "/exit": Closes the server with the given code ("/exit?code=some-code"). The "code"
73-
is expected to be an integer [0-127] or empty; if it is not, it will return an error message.
73+
- "/exit": Closes the server with the given code and graceful shutdown. The endpoint's parameters
74+
are:
75+
- "code": The exit code for the process. Default value: 0. Allows an integer [0-127].
76+
- "timeout": The amount of time to wait for connections to close before shutting down.
77+
Acceptable values are golang durations. If 0 the process will exit immediately without
78+
shutdown.
79+
- "wait": The amount of time to wait before starting shutdown. Acceptable values are
80+
golang durations. If 0 the process will start shutdown immediately.
7481
- "/healthz": Returns "200 OK" if the server is ready, "412 Status Precondition Failed"
7582
otherwise. The server is considered not ready if the UDP server did not start yet or
7683
it exited.
@@ -127,25 +134,27 @@ func (a *atomicBool) get() bool {
127134
}
128135

129136
func main(cmd *cobra.Command, args []string) {
137+
exitCh := make(chan shutdownRequest)
138+
addRoutes(exitCh)
139+
130140
go startUDPServer(udpPort)
131141
if sctpPort != -1 {
132142
go startSCTPServer(sctpPort)
133143
}
134144

135-
addRoutes()
145+
server := &http.Server{Addr: fmt.Sprintf(":%d", httpPort)}
136146
if len(certFile) > 0 {
137-
// only start HTTPS server if a cert is provided
138-
startHTTPSServer(httpPort, certFile, privKeyFile)
147+
startServer(server, exitCh, func() error { return server.ListenAndServeTLS(certFile, privKeyFile) })
139148
} else {
140-
startHTTPServer(httpPort)
149+
startServer(server, exitCh, server.ListenAndServe)
141150
}
142151
}
143152

144-
func addRoutes() {
153+
func addRoutes(exitCh chan shutdownRequest) {
145154
http.HandleFunc("/", rootHandler)
146155
http.HandleFunc("/clientip", clientIPHandler)
147156
http.HandleFunc("/echo", echoHandler)
148-
http.HandleFunc("/exit", exitHandler)
157+
http.HandleFunc("/exit", func(w http.ResponseWriter, req *http.Request) { exitHandler(w, req, exitCh) })
149158
http.HandleFunc("/hostname", hostnameHandler)
150159
http.HandleFunc("/shell", shellHandler)
151160
http.HandleFunc("/upload", uploadHandler)
@@ -156,12 +165,23 @@ func addRoutes() {
156165
http.HandleFunc("/shutdown", shutdownHandler)
157166
}
158167

159-
func startHTTPSServer(httpsPort int, certFile, privKeyFile string) {
160-
log.Fatal(http.ListenAndServeTLS(fmt.Sprintf(":%d", httpPort), certFile, privKeyFile, nil))
161-
}
168+
func startServer(server *http.Server, exitCh chan shutdownRequest, fn func() error) {
169+
go func() {
170+
re := <-exitCh
171+
ctx, cancelFn := context.WithTimeout(context.Background(), re.timeout)
172+
defer cancelFn()
173+
err := server.Shutdown(ctx)
174+
log.Printf("Graceful shutdown completed with: %v", err)
175+
os.Exit(re.code)
176+
}()
162177

163-
func startHTTPServer(httpPort int) {
164-
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", httpPort), nil))
178+
if err := fn(); err != nil {
179+
if err == http.ErrServerClosed {
180+
// wait until the goroutine calls os.Exit()
181+
select {}
182+
}
183+
log.Fatal(err)
184+
}
165185
}
166186

167187
func rootHandler(w http.ResponseWriter, r *http.Request) {
@@ -179,13 +199,37 @@ func clientIPHandler(w http.ResponseWriter, r *http.Request) {
179199
fmt.Fprintf(w, r.RemoteAddr)
180200
}
181201

182-
func exitHandler(w http.ResponseWriter, r *http.Request) {
183-
log.Printf("GET /exit?code=%s", r.FormValue("code"))
184-
code, err := strconv.Atoi(r.FormValue("code"))
185-
if err == nil || r.FormValue("code") == "" {
202+
type shutdownRequest struct {
203+
code int
204+
timeout time.Duration
205+
}
206+
207+
func exitHandler(w http.ResponseWriter, r *http.Request, exitCh chan<- shutdownRequest) {
208+
waitString := r.FormValue("wait")
209+
timeoutString := r.FormValue("timeout")
210+
codeString := r.FormValue("code")
211+
log.Printf("GET /exit?code=%s&timeout=%s&wait=%s", codeString, timeoutString, waitString)
212+
timeout, err := time.ParseDuration(timeoutString)
213+
if err != nil && timeoutString != "" {
214+
fmt.Fprintf(w, "argument 'timeout' must be a valid golang duration or empty, got %q\n", timeoutString)
215+
return
216+
}
217+
wait, err := time.ParseDuration(waitString)
218+
if err != nil && waitString != "" {
219+
fmt.Fprintf(w, "argument 'wait' must be a valid golang duration or empty, got %q\n", waitString)
220+
return
221+
}
222+
code, err := strconv.Atoi(codeString)
223+
if err != nil && codeString != "" {
224+
fmt.Fprintf(w, "argument 'code' must be an integer [0-127] or empty, got %q\n", codeString)
225+
return
226+
}
227+
log.Printf("Will begin shutdown in %s, allowing %s for connections to close, then will exit with %d", wait, timeout, code)
228+
time.Sleep(wait)
229+
if timeout == 0 {
186230
os.Exit(code)
187231
}
188-
fmt.Fprintf(w, "argument 'code' must be an integer [0-127] or empty, got %q", r.FormValue("code"))
232+
exitCh <- shutdownRequest{code: code, timeout: timeout}
189233
}
190234

191235
func hostnameHandler(w http.ResponseWriter, r *http.Request) {

0 commit comments

Comments
 (0)