github.com/Azure/aad-pod-identity@v1.8.17/pkg/nmi/server/server.go (about) 1 package server 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "io" 8 "net" 9 "net/http" 10 "os" 11 "os/signal" 12 "regexp" 13 "runtime" 14 "runtime/debug" 15 "strconv" 16 "strings" 17 "syscall" 18 "time" 19 20 "github.com/Azure/go-autorest/autorest/adal" 21 "github.com/gorilla/mux" 22 "k8s.io/klog/v2" 23 24 "github.com/Azure/aad-pod-identity/pkg/auth" 25 "github.com/Azure/aad-pod-identity/pkg/k8s" 26 "github.com/Azure/aad-pod-identity/pkg/metrics" 27 "github.com/Azure/aad-pod-identity/pkg/nmi" 28 "github.com/Azure/aad-pod-identity/pkg/nmi/conntrack" 29 "github.com/Azure/aad-pod-identity/pkg/nmi/iptables" 30 "github.com/Azure/aad-pod-identity/pkg/pod" 31 ) 32 33 const ( 34 localhost = "127.0.0.1" 35 // "/metadata" portion is case-insensitive in IMDS 36 tokenPathPrefix = "/{type:(?i:metadata)}/identity/oauth2/token" // #nosec 37 hostTokenPathPrefix = "/host/token" 38 // "/metadata" portion is case-insensitive in IMDS 39 instancePathPrefix = "/{type:(?i:metadata)}/instance" // #nosec 40 headerRetryAfter = "Retry-After" 41 ) 42 43 var ( 44 // invalidTokenPathMatcher matches the token path that is not supported by IMDS 45 // this handler is configured right after the token path handler to block requests with 46 // invalid token path instead of sending it to IMDS. 47 // we don't have to handle case sensitivity for "/identity/" as that's rejected by IMDS 48 invalidTokenPathMatcher = mux.MatcherFunc(func(req *http.Request, rm *mux.RouteMatch) bool { 49 r := regexp.MustCompile("/(?i:metadata)/identity(.*?)oauth2(.*?)token") // #nosec 50 return r.MatchString(req.URL.Path) 51 }) 52 ) 53 54 // Server encapsulates all of the parameters necessary for starting up 55 // the server. These can be set via command line. 56 type Server struct { 57 KubeClient k8s.Client 58 NMIHost string 59 NMIPort string 60 MetadataIP string 61 MetadataPort string 62 NodeName string 63 IPTableUpdateTimeIntervalInSeconds int 64 MICNamespace string 65 Initialized bool 66 BlockInstanceMetadata bool 67 MetadataHeaderRequired bool 68 SetRetryAfterHeader bool 69 EnableConntrackDeletion bool 70 // TokenClient is client that fetches identities and tokens 71 TokenClient nmi.TokenClient 72 Reporter *metrics.Reporter 73 } 74 75 // NMIResponse is the response returned to caller 76 type NMIResponse struct { 77 Token msiResponse `json:"token"` 78 ClientID string `json:"clientid"` 79 } 80 81 // MetadataResponse represents the error returned 82 // to caller when metadata header is not specified. 83 type MetadataResponse struct { 84 Error string `json:"error"` 85 ErrorDescription string `json:"error_description"` 86 } 87 88 // NewServer will create a new Server with default values. 89 func NewServer(micNamespace string, blockInstanceMetadata, metadataHeaderRequired, setRetryAfterHeader bool) *Server { 90 reporter, err := metrics.NewReporter() 91 if err != nil { 92 klog.Errorf("failed to create reporter for metrics, error: %+v", err) 93 } else { 94 // keeping this reference to be used in ServeHTTP, as server is not accessible in ServeHTTP 95 appHandlerReporter = reporter 96 auth.InitReporter(reporter) 97 } 98 return &Server{ 99 MICNamespace: micNamespace, 100 BlockInstanceMetadata: blockInstanceMetadata, 101 MetadataHeaderRequired: metadataHeaderRequired, 102 Reporter: reporter, 103 SetRetryAfterHeader: setRetryAfterHeader, 104 } 105 } 106 107 // Run runs the specified Server. 108 func (s *Server) Run() error { 109 go s.updateIPTableRules() 110 111 rtr := mux.NewRouter() 112 // Flow for the request is as follows: 113 // 1. If the request is for token, then it will be handled by tokenHandler post validation. 114 // 2. If the request is for token but the path is invalid, then it will be handled by invalidTokenPathHandler. 115 // 3. If the request is for host token, then it will be handled by hostTokenHandler. 116 // 4. If the request is for instance metadata 117 // 4.1 If blockInstanceMetadata is set to true, then it will be handled by blockInstanceMetadataHandler (deny access to instance metadata). 118 // 5. If the request is for any other path, it will be proxied to IMDS and the response will be returned to the caller. 119 rtr.PathPrefix(tokenPathPrefix).Handler(appHandler(s.msiHandler)) 120 rtr.MatcherFunc(invalidTokenPathMatcher).HandlerFunc(invalidTokenPathHandler) 121 rtr.PathPrefix(hostTokenPathPrefix).Handler(appHandler(s.hostHandler)) 122 if s.BlockInstanceMetadata { 123 rtr.PathPrefix(instancePathPrefix).HandlerFunc(forbiddenHandler) 124 } 125 rtr.PathPrefix("/").HandlerFunc(s.defaultPathHandler) 126 127 klog.Infof("listening on %s:%s", s.NMIHost, s.NMIPort) 128 if err := http.ListenAndServe(fmt.Sprintf("%s:%s", s.NMIHost, s.NMIPort), rtr); err != nil { 129 klog.Fatalf("error creating http server: %+v", err) 130 } 131 return nil 132 } 133 134 func (s *Server) updateIPTableRulesInternal() { 135 target := s.NMIHost 136 if target == "0.0.0.0" { 137 // if we're binding to all interfaces, we still want to add iptables rules for localhost only 138 target = localhost 139 } 140 141 klog.V(5).Infof("node(%s) ip(%s) metadata address(%s:%s) nmi port(%s)", s.NodeName, target, s.MetadataIP, s.MetadataPort, s.NMIPort) 142 143 if err := iptables.AddCustomChain(s.MetadataIP, s.MetadataPort, target, s.NMIPort); err != nil { 144 klog.Fatalf("%s", err) 145 } 146 if err := iptables.LogCustomChain(); err != nil { 147 klog.Fatalf("%s", err) 148 } 149 } 150 151 // try to delete pre-existing conntrack entries for metadata endpoint 152 func (s *Server) deleteConntrackEntries() { 153 klog.Infof("deleting conntrack entries for %s:%s", s.MetadataIP, s.MetadataPort) 154 155 if err := conntrack.DeleteConntrackEntries(s.MetadataIP, s.MetadataPort); err != nil { 156 klog.Fatalf("failed to delete conntrack entries for metadata ip: %s", err) 157 } 158 } 159 160 // updateIPTableRules ensures the correct iptable rules are set 161 // such that metadata requests are received by nmi assigned port 162 // NOT originating from HostIP destined to metadata endpoint are 163 // routed to NMI endpoint 164 func (s *Server) updateIPTableRules() { 165 signalChan := make(chan os.Signal, 1) 166 signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGINT) 167 168 ticker := time.NewTicker(time.Second * time.Duration(s.IPTableUpdateTimeIntervalInSeconds)) 169 defer ticker.Stop() 170 // Run once before the waiting on ticker for the rules to take effect 171 // immediately. 172 s.updateIPTableRulesInternal() 173 // delete conntrack entries for pre-existing connections to metadata endpoint 174 if s.EnableConntrackDeletion { 175 s.deleteConntrackEntries() 176 } 177 s.Initialized = true 178 179 loop: 180 for { 181 select { 182 case <-signalChan: 183 handleTermination() 184 break loop 185 186 case <-ticker.C: 187 s.updateIPTableRulesInternal() 188 } 189 } 190 } 191 192 type appHandler func(http.ResponseWriter, *http.Request) string 193 194 type responseWriter struct { 195 http.ResponseWriter 196 statusCode int 197 } 198 199 func (rw *responseWriter) WriteHeader(code int) { 200 rw.statusCode = code 201 rw.ResponseWriter.WriteHeader(code) 202 } 203 204 func newResponseWriter(w http.ResponseWriter) *responseWriter { 205 return &responseWriter{w, http.StatusOK} 206 } 207 208 var appHandlerReporter *metrics.Reporter 209 210 // ServeHTTP implements the net/http server handler interface 211 // and recovers from panics. 212 func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 213 tracker := fmt.Sprintf("req.method=%s reg.path=%s req.remote=%s", r.Method, r.URL.Path, parseRemoteAddr(r.RemoteAddr)) 214 215 // Set the header in advance so that both success as well 216 // as error paths have it set as application/json content type. 217 w.Header().Set("Content-Type", "application/json") 218 start := time.Now() 219 defer func() { 220 var err error 221 if rec := recover(); rec != nil { 222 _, file, line, _ := runtime.Caller(3) 223 stack := string(debug.Stack()) 224 switch t := rec.(type) { 225 case string: 226 err = errors.New(t) 227 case error: 228 err = t 229 default: 230 err = errors.New("unknown error") 231 } 232 klog.Errorf("panic processing request: %+v, file: %s, line: %d, stacktrace: '%s' %s res.status=%d", r, file, line, stack, tracker, http.StatusInternalServerError) 233 http.Error(w, err.Error(), http.StatusInternalServerError) 234 } 235 }() 236 rw := newResponseWriter(w) 237 ns := fn(rw, r) 238 latency := time.Since(start) 239 klog.Infof("status (%d) took %d ns for %s", rw.statusCode, latency.Nanoseconds(), tracker) 240 241 tokenRequest := parseTokenRequest(r) 242 243 if appHandlerReporter != nil { 244 err := appHandlerReporter.ReportOperationAndStatus( 245 r.URL.Path, 246 strconv.Itoa(rw.statusCode), 247 ns, 248 tokenRequest.Resource, 249 metrics.NMIOperationsDurationM.M(metrics.SinceInSeconds(start))) 250 if err != nil { 251 klog.Warningf("failed to report metrics, error: %+v", err) 252 } 253 } 254 } 255 256 func (s *Server) hostHandler(w http.ResponseWriter, r *http.Request) (ns string) { 257 hostIP := parseRemoteAddr(r.RemoteAddr) 258 tokenRequest := parseTokenRequest(r) 259 260 podns, podname := parsePodInfo(r) 261 if podns == "" || podname == "" { 262 klog.Error("missing podname and podns from request") 263 http.Error(w, "missing 'podname' and 'podns' from request header", http.StatusBadRequest) 264 return 265 } 266 // set the ns so it can be used for metrics 267 ns = podns 268 if hostIP != localhost { 269 klog.Errorf("request remote address is not from a host") 270 http.Error(w, "request remote address is not from a host", http.StatusInternalServerError) 271 return 272 } 273 if !tokenRequest.ValidateResourceParamExists() { 274 klog.Warning("parameter resource cannot be empty") 275 http.Error(w, "parameter resource cannot be empty", http.StatusBadRequest) 276 return 277 } 278 279 podID, err := s.TokenClient.GetIdentities(r.Context(), podns, podname, tokenRequest.ClientID, tokenRequest.ResourceID) 280 if err != nil { 281 klog.Errorf("failed to get identities, error: %+v", err) 282 http.Error(w, err.Error(), http.StatusNotFound) 283 return 284 } 285 tokens, err := s.TokenClient.GetTokens(r.Context(), tokenRequest.ClientID, tokenRequest.Resource, *podID) 286 if err != nil { 287 klog.Errorf("failed to get service principal token for pod:%s/%s, error: %+v", podns, podname, err) 288 httpErrorCode := http.StatusForbidden 289 if auth.IsHealthCheckError(err) { 290 // the adal library performs a health check prior to making the token request 291 // if the health check fails, we want to return a 503 instead of 403 292 // for health check failures, the error is not a token refresh error 293 httpErrorCode = http.StatusServiceUnavailable 294 } 295 http.Error(w, err.Error(), httpErrorCode) 296 return 297 } 298 nmiResp := NMIResponse{ 299 Token: newMSIResponse(*tokens[0]), 300 ClientID: podID.Spec.ClientID, 301 } 302 response, err := json.Marshal(nmiResp) 303 if err != nil { 304 klog.Errorf("failed to marshal service principal token and clientid for pod:%s/%s, error: %+v", podns, podname, err) 305 http.Error(w, err.Error(), http.StatusInternalServerError) 306 return 307 } 308 w.Header().Set("Content-Length", strconv.Itoa(len(response))) 309 _, _ = w.Write(response) 310 return 311 } 312 313 // msiResponse marshals in a format that matches the underlying 314 // metadata endpoint more closely. This increases compatibility 315 // with callers built on older versions of adal client libraries. 316 type msiResponse struct { 317 AccessToken string `json:"access_token"` 318 RefreshToken string `json:"refresh_token"` 319 320 ExpiresIn string `json:"expires_in"` 321 ExpiresOn string `json:"expires_on"` 322 NotBefore string `json:"not_before"` 323 324 Resource string `json:"resource"` 325 Type string `json:"token_type"` 326 } 327 328 func newMSIResponse(token adal.Token) msiResponse { 329 return msiResponse{ 330 AccessToken: token.AccessToken, 331 RefreshToken: token.RefreshToken, 332 ExpiresIn: token.ExpiresIn.String(), 333 ExpiresOn: token.ExpiresOn.String(), 334 NotBefore: token.NotBefore.String(), 335 Resource: token.Resource, 336 Type: token.Type, 337 } 338 } 339 340 func (s *Server) isMIC(podNS, rsName string) bool { 341 micRegEx := regexp.MustCompile(`^mic-*`) 342 if strings.EqualFold(podNS, s.MICNamespace) && micRegEx.MatchString(rsName) { 343 return true 344 } 345 return false 346 } 347 348 func (s *Server) getTokenForExceptedPod(rqClientID, rqResource string) ([]byte, int, error) { 349 var token *adal.Token 350 var err error 351 // ClientID is empty, so we are going to use System assigned MSI 352 if rqClientID == "" { 353 klog.Infof("fetching token for system assigned MSI") 354 token, err = auth.GetServicePrincipalTokenFromMSI(rqResource) 355 } else { // User assigned identity usage. 356 klog.Infof("fetching token for user assigned MSI for resource: %s", rqResource) 357 token, err = auth.GetServicePrincipalTokenFromMSIWithUserAssignedID(rqClientID, rqResource) 358 } 359 if err != nil { 360 // TODO: return the right status code based on the error we got from adal. 361 return nil, http.StatusForbidden, fmt.Errorf("failed to get service principal token, error: %+v", err) 362 } 363 response, err := json.Marshal(newMSIResponse(*token)) 364 if err != nil { 365 return nil, http.StatusInternalServerError, fmt.Errorf("failed to marshal service principal token, error: %+v", err) 366 } 367 return response, http.StatusOK, nil 368 } 369 370 // msiHandler uses the remote address to identify the pod ip and uses it 371 // to find a matching client id, and then returns the token sourced through 372 // AAD using adal 373 // if the requests contains client id it validates it against the admin 374 // configured id. 375 func (s *Server) msiHandler(w http.ResponseWriter, r *http.Request) (ns string) { 376 if s.MetadataHeaderRequired && parseMetadata(r) != "true" { 377 klog.Errorf("metadata header is not specified, req.method=%s reg.path=%s req.remote=%s", r.Method, r.URL.Path, parseRemoteAddr(r.RemoteAddr)) 378 metadataNotSpecifiedError(w) 379 return 380 } 381 382 podIP := parseRemoteAddr(r.RemoteAddr) 383 tokenRequest := parseTokenRequest(r) 384 385 if podIP == "" { 386 klog.Error("request remote address is empty") 387 http.Error(w, "request remote address is empty", http.StatusInternalServerError) 388 return 389 } 390 if !tokenRequest.ValidateResourceParamExists() { 391 klog.Warning("parameter resource cannot be empty") 392 http.Error(w, "parameter resource cannot be empty", http.StatusBadRequest) 393 return 394 } 395 396 podns, podname, rsName, selectors, err := s.KubeClient.GetPodInfo(podIP) 397 if err != nil { 398 klog.Errorf("failed to get pod info from pod IP: %s, error: %+v", podIP, err) 399 http.Error(w, err.Error(), http.StatusInternalServerError) 400 return 401 } 402 // set ns for using in metrics 403 ns = podns 404 exceptionList, err := s.KubeClient.ListPodIdentityExceptions(podns) 405 if err != nil { 406 klog.Errorf("getting list of AzurePodIdentityException in %s namespace failed with error: %+v", podns, err) 407 http.Error(w, err.Error(), http.StatusInternalServerError) 408 return 409 } 410 411 // If its mic, then just directly get the token and pass back. 412 if pod.IsPodExcepted(selectors.MatchLabels, *exceptionList) || s.isMIC(podns, rsName) { 413 klog.Infof("exception pod %s/%s token handling", podns, podname) 414 response, errorCode, err := s.getTokenForExceptedPod(tokenRequest.ClientID, tokenRequest.Resource) 415 if err != nil { 416 klog.Errorf("failed to get service principal token for pod:%s/%s with error code %d, error: %+v", podns, podname, errorCode, err) 417 http.Error(w, err.Error(), errorCode) 418 return 419 } 420 w.Header().Set("Content-Length", strconv.Itoa(len(response))) 421 _, _ = w.Write(response) 422 return 423 } 424 425 podID, err := s.TokenClient.GetIdentities(r.Context(), podns, podname, tokenRequest.ClientID, tokenRequest.ResourceID) 426 if err != nil { 427 klog.Errorf("failed to get matching identities for pod: %s/%s, error: %+v", podns, podname, err) 428 httpErrorCode := http.StatusNotFound 429 if s.SetRetryAfterHeader { 430 httpErrorCode = http.StatusServiceUnavailable 431 // setting it to 20s to allow MIC to finish processing current cycle and pick up this 432 // pod in the next sync cycle 433 w.Header().Set(headerRetryAfter, "20") 434 } 435 http.Error(w, err.Error(), httpErrorCode) 436 return 437 } 438 439 tokens, err := s.TokenClient.GetTokens(r.Context(), tokenRequest.ClientID, tokenRequest.Resource, *podID) 440 if err != nil { 441 klog.Errorf("failed to get service principal token for pod: %s/%s, error: %+v", podns, podname, err) 442 httpErrorCode := http.StatusForbidden 443 if auth.IsHealthCheckError(err) { 444 // the adal library performs a health check prior to making the token request 445 // if the health check fails, we want to return a 503 instead of 403 446 // for health check failures, the error is not a token refresh error 447 httpErrorCode = http.StatusServiceUnavailable 448 } 449 http.Error(w, err.Error(), httpErrorCode) 450 return 451 } 452 453 var v interface{} 454 if len(tokens) == 1 { 455 v = newMSIResponse(*tokens[0]) 456 } else { 457 var msiResp []msiResponse 458 for _, token := range tokens { 459 msiResp = append(msiResp, newMSIResponse(*token)) 460 } 461 v = msiResp 462 } 463 464 response, err := json.Marshal(v) 465 if err != nil { 466 klog.Errorf("failed to marshal service principal token for pod: %s/%s, error: %+v", podns, podname, err) 467 http.Error(w, err.Error(), http.StatusInternalServerError) 468 return 469 } 470 w.Header().Set("Content-Length", strconv.Itoa(len(response))) 471 _, _ = w.Write(response) 472 return 473 } 474 475 // Error replies to the request without the specified metadata header. 476 // It does not otherwise end the request; the caller should ensure no further 477 // writes are done to w. 478 func metadataNotSpecifiedError(w http.ResponseWriter) { 479 metadataResp := MetadataResponse{ 480 Error: "invalid_request", 481 ErrorDescription: "Required metadata header not specified", 482 } 483 response, err := json.Marshal(metadataResp) 484 if err != nil { 485 klog.Errorf("failed to marshal metadata response, %+v", err) 486 http.Error(w, err.Error(), http.StatusInternalServerError) 487 return 488 } 489 490 w.Header().Set("Content-Type", "application/json; charset=utf-8") 491 w.WriteHeader(http.StatusBadRequest) 492 fmt.Fprintln(w, string(response)) 493 } 494 495 func parseMetadata(r *http.Request) (metadata string) { 496 return r.Header.Get("metadata") 497 } 498 499 func parsePodInfo(r *http.Request) (podns string, podname string) { 500 podns = r.Header.Get("podns") 501 podname = r.Header.Get("podname") 502 503 return podns, podname 504 } 505 506 func parseRemoteAddr(addr string) string { 507 n := strings.IndexByte(addr, ':') 508 if n <= 1 { 509 return "" 510 } 511 hostname := addr[0:n] 512 if net.ParseIP(hostname) == nil { 513 return "" 514 } 515 return hostname 516 } 517 518 // TokenRequest contains the client and resource ID token, as well as what resource the client is trying to access. 519 type TokenRequest struct { 520 // ClientID identifies, by Azure AD client ID, a specific identity to use 521 // when authenticating to Azure AD. It is mutually exclusive with 522 // MsiResourceID. 523 // Example: 77788899-f67e-42e1-9a78-89985f6bff3e 524 ClientID string 525 526 // MsiResourceID identifies, by urlencoded ARM resource ID, a specific 527 // identity to use when authenticating to Azure AD. It is mutually exclusive 528 // with ClientID. 529 // Example: /subscriptions/<subid>/resourcegroups/<resourcegroup>/providers/Microsoft.ManagedIdentity/userAssignedIdentities/<name> 530 ResourceID string 531 532 // Resource is the urlencoded URI of the resource for the requested AD token. 533 // Example: https://vault.azure.net. 534 Resource string 535 } 536 537 // ValidateResourceParamExists returns true if there exists a resource parameter from the request. 538 func (r TokenRequest) ValidateResourceParamExists() bool { 539 // check if resource exists in the request 540 // if resource doesn't exist in the request, then adal libraries will return the same error 541 // IMDS also returns an error with 400 response code if resource parameter is empty 542 // this is done to emulate same behavior observed while requesting token from IMDS 543 return len(r.Resource) != 0 544 } 545 546 func parseTokenRequest(r *http.Request) (request TokenRequest) { 547 vals := r.URL.Query() 548 if vals != nil { 549 // These are mutually exclusive values (client_id, msi_resource_id) 550 request.ClientID = vals.Get("client_id") 551 request.ResourceID = vals.Get("msi_res_id") 552 if len(request.ResourceID) == 0 { 553 request.ResourceID = vals.Get("mi_res_id") 554 } 555 556 request.Resource = vals.Get("resource") 557 } 558 return request 559 } 560 561 // defaultPathHandler creates a new request and returns the response body and code 562 func (s *Server) defaultPathHandler(w http.ResponseWriter, r *http.Request) { 563 if s.MetadataHeaderRequired && parseMetadata(r) != "true" { 564 klog.Errorf("metadata header is not specified, req.method=%s reg.path=%s req.remote=%s", r.Method, r.URL.Path, parseRemoteAddr(r.RemoteAddr)) 565 metadataNotSpecifiedError(w) 566 return 567 } 568 569 client := &http.Client{} 570 req, err := http.NewRequest(r.Method, r.URL.String(), r.Body) 571 if err != nil || req == nil { 572 klog.Errorf("failed creating a new request for %s, error: %+v", r.URL.String(), err) 573 http.Error(w, err.Error(), http.StatusInternalServerError) 574 return 575 } 576 host := fmt.Sprintf("%s:%s", s.MetadataIP, s.MetadataPort) 577 req.Host = host 578 req.URL.Host = host 579 req.URL.Scheme = "http" 580 if r.Header != nil { 581 copyHeader(req.Header, r.Header) 582 } 583 resp, err := client.Do(req) 584 if err != nil { 585 klog.Errorf("failed executing request for %s, error: %+v", req.URL.String(), err) 586 http.Error(w, err.Error(), http.StatusInternalServerError) 587 return 588 } 589 defer resp.Body.Close() 590 591 body, err := io.ReadAll(resp.Body) 592 if err != nil { 593 klog.Errorf("failed to read response body for %s, error: %+v", req.URL.String(), err) 594 http.Error(w, err.Error(), http.StatusInternalServerError) 595 } 596 copyHeader(w.Header(), resp.Header) 597 w.WriteHeader(resp.StatusCode) 598 _, _ = w.Write(body) 599 } 600 601 // forbiddenHandler responds to any request with HTTP 403 Forbidden 602 func forbiddenHandler(w http.ResponseWriter, r *http.Request) { 603 http.Error(w, "Request blocked by AAD Pod Identity NMI", http.StatusForbidden) 604 } 605 606 // invalidTokenPathHandler responds to invalid token requests with HTTP 400 Bad Request 607 func invalidTokenPathHandler(w http.ResponseWriter, r *http.Request) { 608 http.Error(w, "Invalid request", http.StatusBadRequest) 609 } 610 611 func copyHeader(dst, src http.Header) { 612 for k, vv := range src { 613 for _, v := range vv { 614 dst.Add(k, v) 615 } 616 } 617 } 618 619 func handleTermination() { 620 klog.Info("received SIGTERM, shutting down") 621 622 exitCode := 0 623 // clean up iptables 624 if err := iptables.DeleteCustomChain(); err != nil { 625 klog.Errorf("failed to clean up during shutdown, error: %+v", err) 626 exitCode = 1 627 } 628 629 // wait for pod to delete 630 klog.Info("handled termination, awaiting pod deletion") 631 time.Sleep(10 * time.Second) 632 633 klog.Infof("exiting with %v", exitCode) 634 os.Exit(exitCode) 635 }