...

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

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

     1  package config
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io/fs"
     9  	"os"
    10  	"path/filepath"
    11  	"strconv"
    12  	"strings"
    13  
    14  	corev1 "k8s.io/api/core/v1"
    15  	kerrors "k8s.io/apimachinery/pkg/api/errors"
    16  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    17  	"sigs.k8s.io/controller-runtime/pkg/client"
    18  
    19  	"edge-infra.dev/pkg/sds/display/constants"
    20  )
    21  
    22  const (
    23  	xServerConfig       = "xserver-config"
    24  	GlobalOpenboxConfig = "global-openbox-config"
    25  
    26  	xorgConfKey          = "xorg.conf"
    27  	openboxConfKey       = "openbox.conf"
    28  	cursorEnabledKey     = "cursor-enabled"
    29  	displayctlEnabledKey = "displayctl-enabled"
    30  
    31  	xorgConfPath = "/etc/X11/xorg.conf"
    32  	mode         = fs.FileMode(0644)
    33  )
    34  
    35  var (
    36  	errXServerConfigMapCannotBeNil       = errors.New("xserver-config ConfigMap cannot be nil")
    37  	errXServerConfigMapNameHasFromFormat = errors.New("xserver-config ConfigMap name should have format 'xserver-config-HOSTNAME'")
    38  	errFlagShouldBeTrueFalse             = errors.New("flag value should be 'true' or 'false'")
    39  	errOpenboxConfCannotBeEmpty          = errors.New("openbox config cannot be empty")
    40  
    41  	openboxConfPath = filepath.Join(os.Getenv("HOME"), ".config/openbox/rc.xml")
    42  )
    43  
    44  // Config allows configuration of the X server.
    45  type Config struct {
    46  	// Hostname for the node the X server is running on.
    47  	Hostname string
    48  
    49  	xorgConf          *string
    50  	openboxConf       *string
    51  	cursorEnabled     *bool
    52  	displayctlEnabled *bool
    53  }
    54  
    55  func New(hostname string) *Config {
    56  	return &Config{
    57  		Hostname: hostname,
    58  	}
    59  }
    60  
    61  // Whether a and b produce the same X server configuration.
    62  //
    63  // Ignores displayctl-enabled as this has no effect on the
    64  // X server configuration.
    65  func Equal(a, b *Config) bool {
    66  	if a == nil || b == nil {
    67  		return a == b
    68  	}
    69  
    70  	if a.XorgConf() != b.XorgConf() {
    71  		return false
    72  	}
    73  
    74  	if a.OpenboxConf() != b.OpenboxConf() {
    75  		return false
    76  	}
    77  
    78  	return a.CursorEnabled() == b.CursorEnabled()
    79  }
    80  
    81  // Returns a deep-copy of the X server config.
    82  func (config *Config) DeepCopy() *Config {
    83  	if config == nil {
    84  		return nil
    85  	}
    86  	dst := new(Config)
    87  	config.DeepCopyInto(dst)
    88  	return dst
    89  }
    90  
    91  // Deep-copies the X server config into the destination.
    92  func (config *Config) DeepCopyInto(dst *Config) {
    93  	// the logic here is the same as controller-gen's zz_generated.deepcopy.go
    94  	*dst = *config
    95  	if config.xorgConf != nil {
    96  		dst, config := &dst.xorgConf, &config.xorgConf
    97  		*dst = new(string)
    98  		**dst = **config
    99  	}
   100  	if config.openboxConf != nil {
   101  		dst, config := &dst.openboxConf, &config.openboxConf
   102  		*dst = new(string)
   103  		**dst = **config
   104  	}
   105  	if config.cursorEnabled != nil {
   106  		dst, config := &dst.cursorEnabled, &config.cursorEnabled
   107  		*dst = new(bool)
   108  		**dst = **config
   109  	}
   110  	if config.displayctlEnabled != nil {
   111  		dst, config := &dst.displayctlEnabled, &config.displayctlEnabled
   112  		*dst = new(bool)
   113  		**dst = **config
   114  	}
   115  }
   116  
   117  // xorg.conf configuration provided to the X server.
   118  //
   119  // Defaults to empty if if has not been configured.
   120  func (config *Config) XorgConf() string {
   121  	if config.xorgConf == nil {
   122  		return ""
   123  	}
   124  	return *config.xorgConf
   125  }
   126  
   127  func (config *Config) SetXorgConf(xorgConf string) {
   128  	config.xorgConf = &xorgConf
   129  }
   130  
   131  // Updates /etc/X11/xorg.conf with the configured Xorg config.
   132  func (config *Config) UpdateXorgConf(context.Context) error {
   133  	return safeUpdateFile(xorgConfPath, config.XorgConf(), mode)
   134  }
   135  
   136  // openbox.conf configuration provided to the X server.
   137  //
   138  // Defaults to empty if if has not been configured.
   139  func (config *Config) OpenboxConf() string {
   140  	if config.openboxConf == nil {
   141  		return ""
   142  	}
   143  	return *config.openboxConf
   144  }
   145  
   146  func (config *Config) SetOpenboxConf(openboxConf string) {
   147  	config.openboxConf = &openboxConf
   148  }
   149  
   150  // Updates $HOME/.config/openbox/rc.xml with the configured Openbox config.
   151  func (config *Config) UpdateOpenboxConf(context.Context) error {
   152  	return safeUpdateFile(openboxConfPath, config.OpenboxConf(), mode)
   153  }
   154  
   155  // Whether the cursor is enabled.
   156  //
   157  // Defaults to cursor disabled if it has not been configured.
   158  func (config *Config) CursorEnabled() bool {
   159  	if config.cursorEnabled == nil {
   160  		return false
   161  	}
   162  	return *config.cursorEnabled
   163  }
   164  
   165  func (config *Config) SetCursorEnabled(cursorEnabled bool) {
   166  	config.cursorEnabled = &cursorEnabled
   167  }
   168  
   169  // Whether displayctl is enabled.
   170  //
   171  // When not configured, displayctl will only be enabled when XorgConf has
   172  // not been defined.
   173  func (config *Config) DisplayctlEnabled() bool {
   174  	if config.displayctlEnabled != nil {
   175  		return *config.displayctlEnabled
   176  	}
   177  	return config.xorgConf == nil
   178  }
   179  
   180  func (config *Config) SetDisplayctlEnabled(displayctlEnabled bool) {
   181  	config.displayctlEnabled = &displayctlEnabled
   182  }
   183  
   184  // The X server config in ConfigMap form.
   185  //
   186  // If config fields are nil, they will be omitted from the ConfigMap keys.
   187  func (config *Config) ToConfigMap() *corev1.ConfigMap {
   188  	configMap := &corev1.ConfigMap{
   189  		ObjectMeta: metav1.ObjectMeta{
   190  			Namespace: constants.Namespace,
   191  			Name:      config.ConfigMapName(),
   192  		},
   193  		Data: map[string]string{},
   194  	}
   195  
   196  	if config.xorgConf != nil {
   197  		configMap.Data[xorgConfKey] = *config.xorgConf
   198  	}
   199  
   200  	if config.openboxConf != nil {
   201  		configMap.Data[openboxConfKey] = *config.openboxConf
   202  	}
   203  
   204  	if config.cursorEnabled != nil {
   205  		configMap.Data[cursorEnabledKey] = strconv.FormatBool(*config.cursorEnabled)
   206  	}
   207  
   208  	if config.displayctlEnabled != nil {
   209  		configMap.Data[displayctlEnabledKey] = strconv.FormatBool(*config.displayctlEnabled)
   210  	}
   211  
   212  	return configMap
   213  }
   214  
   215  // The name of the X server config's ConfigMap, i.e. "xserver-config-HOSTNAME".
   216  func (config *Config) ConfigMapName() string {
   217  	return ConfigMapNameFromHostname(config.Hostname)
   218  }
   219  
   220  // Updates the X server config's ConfigMap in the cluster using the k8s
   221  // client, creating it if it does not exist.
   222  func (config *Config) UpdateConfigMap(ctx context.Context, c client.Client) error {
   223  	configMap := config.ToConfigMap()
   224  
   225  	currentConfigMap, err := ConfigMapFromClient(ctx, config.Hostname, c)
   226  	if kerrors.IsNotFound(err) {
   227  		return c.Create(ctx, configMap)
   228  	} else if err != nil {
   229  		return err
   230  	}
   231  
   232  	return c.Patch(ctx, configMap, client.MergeFrom(currentConfigMap))
   233  }
   234  
   235  // Creates an X server config from a ConfigMap.
   236  //
   237  // If config keys are missing they will be set to nil.
   238  func FromConfigMap(configMap *corev1.ConfigMap) (*Config, error) {
   239  	if configMap == nil {
   240  		return nil, errXServerConfigMapCannotBeNil
   241  	}
   242  
   243  	config := &Config{}
   244  
   245  	hostname, err := HostnameFromConfigMapName(configMap.GetName())
   246  	if err != nil {
   247  		return nil, err
   248  	}
   249  
   250  	config.Hostname = hostname
   251  
   252  	if configMap.Data == nil {
   253  		return config, nil
   254  	}
   255  
   256  	if xorgConf, ok := configMap.Data[xorgConfKey]; ok {
   257  		config.xorgConf = &xorgConf
   258  	}
   259  
   260  	if openboxConf, ok := configMap.Data[openboxConfKey]; ok {
   261  		config.openboxConf = &openboxConf
   262  	}
   263  
   264  	if cursorEnabled, ok := configMap.Data[cursorEnabledKey]; ok {
   265  		enabled, err := strconv.ParseBool(cursorEnabled)
   266  		if err != nil {
   267  			return nil, fmt.Errorf("%s %w, got: '%s'", cursorEnabledKey, errFlagShouldBeTrueFalse, cursorEnabled)
   268  		}
   269  		config.cursorEnabled = &enabled
   270  	}
   271  
   272  	if dispalyctlEnabled, ok := configMap.Data[displayctlEnabledKey]; ok {
   273  		enabled, err := strconv.ParseBool(dispalyctlEnabled)
   274  		if err != nil {
   275  			return nil, fmt.Errorf("%s %w, got: '%s'", displayctlEnabledKey, errFlagShouldBeTrueFalse, dispalyctlEnabled)
   276  		}
   277  		config.displayctlEnabled = &enabled
   278  	}
   279  
   280  	return config, nil
   281  }
   282  
   283  // Fetches the host's X server ConfigMap with the K8s client and
   284  // returns the config it represents.
   285  func FromClient(ctx context.Context, hostname string, c client.Client) (*Config, error) {
   286  	configMap, err := ConfigMapFromClient(ctx, hostname, c)
   287  	if err != nil {
   288  		return nil, fmt.Errorf("cannot get xserver-config ConfigMap for %s: %w", hostname, err)
   289  	}
   290  
   291  	config, err := FromConfigMap(configMap)
   292  	if err != nil {
   293  		return nil, fmt.Errorf("cannot create config from %s ConfigMap: %w", configMap.GetName(), err)
   294  	}
   295  
   296  	return config, nil
   297  }
   298  
   299  // Fetches the host's X server ConfigMap with the K8s client.
   300  //
   301  // nolint: revive
   302  func ConfigMapFromClient(ctx context.Context, hostname string, c client.Client) (*corev1.ConfigMap, error) {
   303  	key := client.ObjectKey{
   304  		Namespace: constants.Namespace,
   305  		Name:      ConfigMapNameFromHostname(hostname),
   306  	}
   307  
   308  	configMap := &corev1.ConfigMap{}
   309  	if err := c.Get(ctx, key, configMap); err != nil {
   310  		return nil, err
   311  	}
   312  
   313  	return configMap, nil
   314  }
   315  
   316  // Fetches the global Openbox config from the global-openbox-config ConfigMap.
   317  func GlobalOpenboxConfFromClient(ctx context.Context, c client.Client) (string, error) {
   318  	key := client.ObjectKey{
   319  		Namespace: constants.Namespace,
   320  		Name:      GlobalOpenboxConfig,
   321  	}
   322  
   323  	configMap := &corev1.ConfigMap{}
   324  	if err := c.Get(ctx, key, configMap); err != nil {
   325  		return "", err
   326  	}
   327  
   328  	openboxConf := ""
   329  	if configMap.Data != nil {
   330  		openboxConf = configMap.Data[openboxConfKey]
   331  	}
   332  
   333  	if openboxConf == "" {
   334  		return "", errOpenboxConfCannotBeEmpty
   335  	}
   336  
   337  	return openboxConf, nil
   338  }
   339  
   340  // Generates the X server config ConfigMap name for a hostname.
   341  //
   342  // nolint: revive
   343  func ConfigMapNameFromHostname(hostname string) string {
   344  	return fmt.Sprintf("%s-%s", xServerConfig, hostname)
   345  }
   346  
   347  // Parses the X server config ConfigMap name, returning the hostname.
   348  //
   349  // Returns an error name if not in format "xserver-config-HOSTNAME".
   350  func HostnameFromConfigMapName(name string) (string, error) {
   351  	hostname := strings.TrimPrefix(name, fmt.Sprintf("%s-", xServerConfig))
   352  	if name == hostname {
   353  		return "", fmt.Errorf("%w, got: '%s'", errXServerConfigMapNameHasFromFormat, name)
   354  	}
   355  	return hostname, nil
   356  }
   357  
   358  // Updates file contents if it has changed.
   359  // If the directory does not exist, it will be created.
   360  func safeUpdateFile(path, contents string, mode fs.FileMode) error {
   361  	if err := os.MkdirAll(filepath.Dir(path), mode); err != nil {
   362  		return err
   363  	}
   364  
   365  	currentData, err := os.ReadFile(path)
   366  	if err != nil && !errors.Is(err, os.ErrNotExist) {
   367  		return err
   368  	}
   369  
   370  	newData := []byte(contents)
   371  	if bytes.Equal(currentData, newData) && currentData != nil {
   372  		return nil
   373  	}
   374  
   375  	return os.WriteFile(path, newData, mode)
   376  }
   377  

View as plain text