...

Source file src/edge-infra.dev/pkg/sds/display/k8s/controllers/xserver/xserver_runnable.go

Documentation: edge-infra.dev/pkg/sds/display/k8s/controllers/xserver

     1  package xserver
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"os"
     9  	"time"
    10  
    11  	"github.com/go-logr/logr"
    12  	ctrl "sigs.k8s.io/controller-runtime"
    13  	"sigs.k8s.io/controller-runtime/pkg/client"
    14  	"sigs.k8s.io/controller-runtime/pkg/healthz"
    15  
    16  	"edge-infra.dev/pkg/sds/display/constants"
    17  	"edge-infra.dev/pkg/sds/display/displaymanager/manager"
    18  	xserverconfig "edge-infra.dev/pkg/sds/display/k8s/controllers/xserver/config"
    19  	"edge-infra.dev/pkg/sds/lib/os/env"
    20  	"edge-infra.dev/pkg/sds/lib/process/processmanager"
    21  )
    22  
    23  const (
    24  	xinitName    = "xinit"
    25  	xinitPath    = "/usr/bin/xinit"
    26  	noCursorFlag = "-nocursor"
    27  
    28  	waitingForConfigMessage = "waiting to receive config... (at least one ConfigMap must be present)"
    29  
    30  	timeout     = time.Second * 10
    31  	minWaitTime = time.Second * 3
    32  )
    33  
    34  var baseXinitArgs = []string{
    35  	"/etc/X11/xinit/Xsession",
    36  	"openbox-session",
    37  	"--",
    38  	"/usr/bin/X",
    39  	":0",
    40  	"vt7",
    41  	"-logfile", "/dev/stdout",
    42  	"-logverbose", env.New().Get("LOGLVL", "3"),
    43  }
    44  
    45  var (
    46  	errUseGetForHealthz = fmt.Errorf("expected GET for healthz check")
    47  	errSocketNotReady   = fmt.Errorf("%s socket is not ready", constants.X11Socket)
    48  )
    49  
    50  func configMapNamespaceNames(hostname string) []string {
    51  	return []string{
    52  		fmt.Sprintf("%s/%s", constants.Namespace, xserverconfig.GlobalOpenboxConfig),
    53  		fmt.Sprintf("%s/%s", constants.Namespace, xserverconfig.ConfigMapNameFromHostname(hostname)),
    54  	}
    55  }
    56  
    57  // Runnable is a thread which manages the X process. It will start the xinit process
    58  // and restart it on any changes to the configuration sent from the config controller.
    59  type Runnable struct {
    60  	Name     string
    61  	Hostname string
    62  
    63  	// Handles the lifecycle of the xinit process.
    64  	processmanager.Process
    65  
    66  	// Config configures the X server.
    67  	*xserverconfig.Config
    68  	// configChan is used to receive configuration from the X server config controller.
    69  	configChan configChannel
    70  
    71  	client  client.Client
    72  	healthz healthz.Checker
    73  }
    74  
    75  func NewXServerRunnable(displayManager manager.DisplayManager, configChan configChannel, c client.Client, log logr.Logger) (*Runnable, error) {
    76  	config := &xserverconfig.Config{}
    77  
    78  	proc, err := processmanager.NewProcess(xinitName, xinitPath)
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  
    83  	healthz := createHealthzCheck(log)
    84  	readyCheck := createReadyCheck(displayManager)
    85  
    86  	proc.WithLogger(log, true)
    87  	proc.WithPreStartHooks(config.UpdateXorgConf, config.UpdateOpenboxConf)
    88  	proc.WithReadyCheck(readyCheck)
    89  	proc.WithWaitUntilReady(timeout)
    90  	proc.WithExpectNoExit()
    91  
    92  	return &Runnable{
    93  		Name:       constants.XServerManagerName,
    94  		Hostname:   displayManager.Hostname(),
    95  		Process:    proc,
    96  		Config:     config,
    97  		configChan: configChan,
    98  		client:     c,
    99  		healthz:    healthz,
   100  	}, nil
   101  }
   102  
   103  func createReadyCheck(displayManager manager.DisplayManager) processmanager.ReadyCheckFunc {
   104  	return func(ctx context.Context) (bool, error) {
   105  		if err := displayManager.Wait(ctx); err != nil {
   106  			return false, nil
   107  		}
   108  		return true, nil
   109  	}
   110  }
   111  
   112  func createHealthzCheck(log logr.Logger) healthz.Checker {
   113  	return func(req *http.Request) error {
   114  		if req.Method != http.MethodGet {
   115  			return fmt.Errorf("%w, got: %s", errUseGetForHealthz, req.Method)
   116  		}
   117  		if _, err := os.Stat(constants.X11Socket); err != nil {
   118  			err = fmt.Errorf("%w: %w", errSocketNotReady, err)
   119  			log.Error(err, "healthz check failed")
   120  			return err
   121  		}
   122  		return nil
   123  	}
   124  }
   125  
   126  func (r *Runnable) SetupWithManager(mgr ctrl.Manager) error {
   127  	if err := mgr.Add(r); err != nil {
   128  		return err
   129  	}
   130  	return mgr.AddHealthzCheck("xserver-status", r.healthz)
   131  }
   132  
   133  // +kubebuilder:rbac:groups=display.edge.ncr.com,resources=nodedisplayconfigs,verbs=create;get;list;watch
   134  
   135  // Runs the xinit process, restarting it on any changes to X server configuration.
   136  //
   137  // Exits when context is cancelled or any errors are returned from the process manager.
   138  func (r *Runnable) Start(ctx context.Context) (err error) {
   139  	log := ctrl.LoggerFrom(ctx).WithName(r.Name)
   140  	log.Info("starting xserver")
   141  	log.Info(waitingForConfigMessage, "configMaps", configMapNamespaceNames(r.Hostname))
   142  
   143  	defer func() {
   144  		log.Info("waiting for X to shutdown")
   145  		stopCtx, cancel := context.WithTimeout(context.Background(), timeout)
   146  		defer cancel()
   147  		err = errors.Join(err, r.WaitUntilStopped(stopCtx))
   148  	}()
   149  
   150  	for {
   151  		select {
   152  		case config := <-r.configChan:
   153  			log.Info("received new config, restarting X")
   154  			config.DeepCopyInto(r.Config)
   155  			if err := r.restart(ctx); err != nil {
   156  				return err
   157  			}
   158  		case result := <-r.Result():
   159  			return result
   160  		case <-ctx.Done():
   161  			return nil
   162  		}
   163  	}
   164  }
   165  
   166  // Configures xinit arguments and restarts the xinit process.
   167  func (r *Runnable) restart(ctx context.Context) error {
   168  	args := xinitArgs(r.CursorEnabled())
   169  	r.WithArgs(args...)
   170  	return r.Restart(ctx)
   171  }
   172  
   173  func xinitArgs(cursorEnabled bool) []string {
   174  	args := baseXinitArgs
   175  
   176  	if !cursorEnabled {
   177  		args = append(args, noCursorFlag)
   178  	}
   179  
   180  	return args
   181  }
   182  

View as plain text