volcano.sh/volcano@v1.9.0/pkg/util/socket.go (about)

     1  /*
     2  Copyright 2023 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package util
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"html"
    23  	"net"
    24  	"net/http"
    25  	"os"
    26  	"path/filepath"
    27  	"strconv"
    28  	"sync"
    29  	"time"
    30  
    31  	"golang.org/x/sys/unix"
    32  	"k8s.io/apimachinery/pkg/util/runtime"
    33  	"k8s.io/klog/v2"
    34  )
    35  
    36  const (
    37  	DefaultSocketDir = "/tmp/klog-socks" // Default directory storing socket files
    38  	SocketSuffix     = "-klog.sock"
    39  	SocketDirEnvName = "DEBUG_SOCKET_DIR"
    40  	// The HTTP request patterns
    41  	setLogLevelPath  = "/setlevel"
    42  	getLogLevelPath  = "/getlevel"
    43  	exampleSocketCli = "\"Failed to change klog log level, because got wrong value from level argument\\n\"+\n\t\t\t\t\"example: curl --unix-socket /tmp/klog-socks/componentName-klog.sock \\\"http://localhost/setlevel?level=8&duration=60s\\\"\\n\"+\n\t\t\t\t\"level=8 means changing klog log level to 8\\n\"+\n\t\t\t\t\"duration=60s means maintaining level=8 for 60 seconds[60m -> 60 minutes; 60h -> 60 hours]\""
    44  )
    45  
    46  var (
    47  	// When users frequently make request to change klog log level, the previously registered timer may not expire.
    48  	// To improve performance, cancel the previous timer. prevCtx, prevCtxCancelFunc is used to achieve this target.
    49  	prevCtx           context.Context
    50  	prevCtxCancelFunc context.CancelFunc
    51  
    52  	// currentLogLevel stores current log level
    53  	currentLogLevel string
    54  	// startupLogLevel stores start-up log level
    55  	startupLogLevel string
    56  	// mutex is used to avoid data race about prevCtx, prevCtxCancelFunc and currentLogLevel
    57  	mutex sync.RWMutex
    58  )
    59  
    60  // responseOk returns a statusOK response to client
    61  func responseOk(w *http.ResponseWriter, okMsg string) {
    62  	(*w).Header().Set("Content-Type", "text/plain; charset=utf-8")
    63  	(*w).Header().Set("X-Content-Type-Options", "nosniff")
    64  	_, err := fmt.Fprint(*w, okMsg)
    65  	if err != nil {
    66  		klog.Error(err)
    67  		return
    68  	}
    69  }
    70  
    71  // responseError returns an error response containing specific httpCode and errMsg to client
    72  func responseError(w *http.ResponseWriter, errMsg string, httpCode int) {
    73  	http.Error(*w, errMsg, httpCode)
    74  }
    75  
    76  // modifyLoglevel will try to change current klog's log level to newLogLevel and assign it to currentLogLevel.
    77  // After prevCtxCancelFunc function corresponding to last timer executed, prevCtx and prevCtxCancelFunc will be reassigned
    78  // in order to represent brand-new timer.
    79  func modifyLoglevel(newLogLevel string) error {
    80  	mutex.Lock()
    81  	defer mutex.Unlock()
    82  
    83  	// Change klog log level to new value
    84  	var loglevel klog.Level
    85  	if err := loglevel.Set(newLogLevel); err != nil {
    86  		return err
    87  	}
    88  	currentLogLevel = newLogLevel
    89  
    90  	// Cancel the previous timer.
    91  	if prevCtxCancelFunc != nil {
    92  		prevCtxCancelFunc()
    93  	}
    94  	prevCtx, prevCtxCancelFunc = context.WithCancel(context.Background())
    95  	return nil
    96  }
    97  
    98  // reset creates a timer to make klog recover to start-up log level.
    99  func reset(ctx context.Context, duration time.Duration) {
   100  	defer runtime.HandleCrash()
   101  	select {
   102  	// Create a timer
   103  	case <-time.After(duration):
   104  		var loglevel klog.Level
   105  		mutex.Lock()
   106  		defer mutex.Unlock()
   107  		if err := loglevel.Set(startupLogLevel); err != nil {
   108  			klog.Error(err)
   109  			return
   110  		}
   111  		currentLogLevel = startupLogLevel
   112  		klog.InfoS("Klog recover to start-up log level successfully", "startupLogLevel", startupLogLevel)
   113  	// Cancel previous timer
   114  	case <-ctx.Done():
   115  		klog.InfoS("Cancel previous timer successfully")
   116  	}
   117  }
   118  
   119  // installKlogLogLevelHandler registers the HTTP request patterns that can set/get current klog log level
   120  func installKlogLogLevelHandler(mux *http.ServeMux, startup string) {
   121  	currentLogLevel, startupLogLevel = startup, startup
   122  	// Register the HTTP request patterns that can change klog log level
   123  	mux.Handle(setLogLevelPath, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   124  		values := r.URL.Query()
   125  		rawLevel := values.Get("level")
   126  		rawDuration := values.Get("duration")
   127  		// Escape the data that needs to be output to the log, Prevent Reflected cross-site scripting
   128  		rawLevel = html.EscapeString(rawLevel)
   129  		rawDuration = html.EscapeString(rawDuration)
   130  		var duration time.Duration
   131  		var err error
   132  		// Validate argument in request
   133  		if level, err := strconv.ParseInt(rawLevel, 10, 64); err != nil || level <= 0 {
   134  			responseError(&w, exampleSocketCli, http.StatusBadRequest)
   135  			return
   136  		}
   137  		if duration, err = time.ParseDuration(rawDuration); err != nil || duration.Milliseconds() <= 0 {
   138  			responseError(&w, exampleSocketCli, http.StatusBadRequest)
   139  			return
   140  		}
   141  
   142  		if err := modifyLoglevel(rawLevel); err != nil {
   143  			responseError(&w, fmt.Sprintf("Failed to change klog log level. Error: %v\n", err.Error()), http.StatusInternalServerError)
   144  			return
   145  		}
   146  
   147  		mutex.RLock()
   148  		// Create a timer to make klog recover to start-up log level.
   149  		// There will be more than one timer using same prevCtx variable under extreme conditions.
   150  		// Therefore, put reset function in mutex range.
   151  		go reset(prevCtx, duration)
   152  		responseOk(&w, fmt.Sprintf("Change klog log level to %s successfully and  for %v\n", currentLogLevel, duration))
   153  		mutex.RUnlock()
   154  	}))
   155  
   156  	// Register the HTTP request patterns that can get current klog log level
   157  	mux.Handle(getLogLevelPath, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   158  		mutex.RLock()
   159  		responseOk(&w, fmt.Sprintf("Current klog log level: %s\n", currentLogLevel))
   160  		mutex.RUnlock()
   161  	}))
   162  }
   163  
   164  // listenUnix does net.Listen for a unix socket
   165  func listenUnix(componentName string, socketDir string) (net.Listener, error) {
   166  	// Use default directory to store socket files
   167  	if len(socketDir) == 0 {
   168  		socketDir = DefaultSocketDir
   169  	}
   170  
   171  	// Check whether KlogLogLevelSocketDir exists
   172  	if _, err := os.Stat(socketDir); os.IsNotExist(err) {
   173  		if err = os.MkdirAll(socketDir, 0750); err != nil {
   174  			return nil, fmt.Errorf("error creating klog log level socket dir: %v", err)
   175  		}
   176  	}
   177  
   178  	// Specify socket file full path
   179  	socketFileFullName := componentName + SocketSuffix
   180  	socketFileFullPath := filepath.Join(socketDir, socketFileFullName)
   181  
   182  	// Remove any socket, stale or not, but fall through for other files
   183  	fi, err := os.Stat(socketFileFullPath)
   184  	if err == nil && (fi.Mode()&os.ModeSocket) != 0 {
   185  		err := os.Remove(socketFileFullPath)
   186  		if err != nil {
   187  			klog.ErrorS(err, "failed to remote socket file", "file", socketFileFullPath)
   188  			return nil, err
   189  		}
   190  	}
   191  
   192  	// Default to only user accessible socket, caller can open up later if desired
   193  	// Result perm: 777 - 077 = 700
   194  	oldmask := unix.Umask(0077)
   195  	l, err := net.Listen("unix", socketFileFullPath)
   196  	unix.Umask(oldmask)
   197  
   198  	return l, err
   199  }
   200  
   201  // serveOnListener starts the server using given listener, loops forever.
   202  func serveOnListener(l net.Listener, m *http.ServeMux) error {
   203  	server := http.Server{
   204  		Handler: m,
   205  	}
   206  	return server.Serve(l)
   207  }
   208  
   209  // ListenAndServeKlogLogLevel registers a server on specific component to handle the HTTP request which set/get klog log level
   210  func ListenAndServeKlogLogLevel(componentName string, startupLogLevel string, socketDir string) {
   211  	var err error
   212  	defer runtime.HandleCrash()
   213  
   214  	mux := http.NewServeMux()
   215  	installKlogLogLevelHandler(mux, startupLogLevel)
   216  
   217  	var listener net.Listener
   218  	listener, err = listenUnix(componentName, socketDir)
   219  	if err != nil {
   220  		return
   221  	}
   222  
   223  	if err = serveOnListener(listener, mux); err != nil {
   224  		return
   225  	}
   226  }