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
45 type Config struct {
46
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
62
63
64
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
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
92 func (config *Config) DeepCopyInto(dst *Config) {
93
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
118
119
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
132 func (config *Config) UpdateXorgConf(context.Context) error {
133 return safeUpdateFile(xorgConfPath, config.XorgConf(), mode)
134 }
135
136
137
138
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
151 func (config *Config) UpdateOpenboxConf(context.Context) error {
152 return safeUpdateFile(openboxConfPath, config.OpenboxConf(), mode)
153 }
154
155
156
157
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
170
171
172
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
185
186
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
216 func (config *Config) ConfigMapName() string {
217 return ConfigMapNameFromHostname(config.Hostname)
218 }
219
220
221
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
236
237
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
284
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
300
301
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
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
341
342
343 func ConfigMapNameFromHostname(hostname string) string {
344 return fmt.Sprintf("%s-%s", xServerConfig, hostname)
345 }
346
347
348
349
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
359
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