-
Notifications
You must be signed in to change notification settings - Fork 4
/
main.go
584 lines (555 loc) · 22.1 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
package main
import (
"encoding/json"
"errors"
"fmt"
"os"
"time"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/twpayne/go-vfs/v4"
"github.com/twpayne/go-vfsafero/v4"
"gopkg.in/yaml.v3"
corev1 "k8s.io/api/core/v1"
"k8s.io/utils/ptr"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
infrastructurev1beta1 "github.com/rancher-sandbox/cluster-api-provider-elemental/api/v1beta1"
"github.com/rancher-sandbox/cluster-api-provider-elemental/internal/agent/client"
"github.com/rancher-sandbox/cluster-api-provider-elemental/internal/agent/config"
"github.com/rancher-sandbox/cluster-api-provider-elemental/internal/agent/hostname"
log "github.com/rancher-sandbox/cluster-api-provider-elemental/internal/agent/log"
"github.com/rancher-sandbox/cluster-api-provider-elemental/internal/agent/utils"
"github.com/rancher-sandbox/cluster-api-provider-elemental/internal/api"
"github.com/rancher-sandbox/cluster-api-provider-elemental/internal/identity"
"github.com/rancher-sandbox/cluster-api-provider-elemental/internal/version"
"github.com/rancher-sandbox/cluster-api-provider-elemental/pkg/agent/osplugin"
)
const (
configPathDefault = "/etc/elemental/agent/config.yaml"
bootstrapSentinelFile = "/run/cluster-api/bootstrap-success.complete"
)
// Flags.
var (
versionFlag bool
resetFlag bool
installFlag bool
registerFlag bool
debugFlag bool
)
// Arguments.
var (
configPath string
)
var (
ErrIncorrectArguments = errors.New("incorrect arguments, run 'elemental-agent --help' for usage")
)
func main() {
fs := vfs.OSFS
osPluginLoader := osplugin.NewLoader()
client := client.NewClient(version.Version)
cmd := newCommand(fs, osPluginLoader, client)
if err := cmd.Execute(); err != nil {
log.Error(err, "running elemental-agent")
os.Exit(1)
}
}
func newCommand(fs vfs.FS, pluginLoader osplugin.Loader, client client.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "elemental-agent",
Short: "Elemental Agent command",
Long: "elemental-agent registers a node with the elemental-operator via a config file",
RunE: func(_ *cobra.Command, args []string) error {
// Display version
if versionFlag {
log.Infof("Agent version %s, commit %s, commit date %s", version.Version, version.Commit, version.CommitDate)
return nil
}
// Sanity checks
if installFlag && resetFlag {
return fmt.Errorf("--install and --reset are mutually exclusive: %w", ErrIncorrectArguments)
}
// Parse config file
conf, err := getConfig(fs)
if err != nil {
return fmt.Errorf("parsing configuration file '%s': %w", configPath, err)
}
// Set debug logs
if conf.Agent.Debug || debugFlag {
log.EnableDebug()
log.Debug("Debug logging enabled")
}
// Initialize WorkDir
if err := utils.CreateDirectory(fs, conf.Agent.WorkDir); err != nil {
return fmt.Errorf("creating work directory '%s': %w", conf.Agent.WorkDir, err)
}
// Initialize Plugin
log.Infof("Loading Plugin: %s", conf.Agent.OSPlugin)
osPlugin, err := pluginLoader.Load(conf.Agent.OSPlugin)
if err != nil {
return fmt.Errorf("Loading plugin '%s': %w", conf.Agent.OSPlugin, err)
}
log.Info("Initializing Plugin")
if err := osPlugin.Init(osplugin.PluginContext{
WorkDir: conf.Agent.WorkDir,
ConfigPath: configPath,
Debug: conf.Agent.Debug || debugFlag,
}); err != nil {
return fmt.Errorf("Initializing plugin: %w", err)
}
// Initialize Identity
identityManager := identity.NewManager(fs, conf.Agent.WorkDir)
identity, err := identityManager.LoadSigningKeyOrCreateNew()
if err != nil {
return fmt.Errorf("initializing identity: %w", err)
}
// Initialize Elemental API Client
if err := client.Init(fs, identity, conf); err != nil {
return fmt.Errorf("initializing Elemental API client: %w", err)
}
// Get current hostname
hostname, err := osPlugin.GetHostname()
if err != nil {
return fmt.Errorf("getting current hostname: %w", err)
}
// Register
if registerFlag {
log.Info("Registering Elemental Host")
pubKey, err := identity.MarshalPublic()
if err != nil {
return fmt.Errorf("marshalling host public key: %w", err)
}
var registration *api.RegistrationResponse
hostname, registration = handleRegistration(client, osPlugin, pubKey, conf.Registration.Token, conf.Agent.Reconciliation)
log.Infof("Successfully registered as '%s'", hostname)
if err := handlePostRegistration(osPlugin, hostname, identity, registration); err != nil {
err = fmt.Errorf("handling post registration: %w", err)
attemptConditionReporting(client, hostname, clusterv1.Condition{
Type: infrastructurev1beta1.RegistrationReady,
Status: corev1.ConditionFalse,
Severity: clusterv1.ConditionSeverityError,
Reason: infrastructurev1beta1.RegistrationFailedReason,
Message: err.Error(),
})
return err
}
attemptConditionReporting(client, hostname, clusterv1.Condition{
Type: infrastructurev1beta1.RegistrationReady,
Status: corev1.ConditionTrue,
Severity: clusterv1.ConditionSeverityInfo,
Reason: "",
Message: "",
})
attemptConditionReporting(client, hostname, clusterv1.Condition{
Type: infrastructurev1beta1.InstallationReady,
Status: corev1.ConditionFalse,
Severity: infrastructurev1beta1.WaitingForInstallationReasonSeverity,
Reason: infrastructurev1beta1.WaitingForInstallationReason,
Message: "Host is registered successfully. Waiting for installation.",
})
// Exit program if --install was not called
if !installFlag {
return nil
}
}
// Install
if installFlag {
log.Info("Installing Elemental")
handleInstall(client, osPlugin, hostname, conf.Registration.Token, conf.Agent.Reconciliation)
log.Info("Installation successful")
handlePost(osPlugin, conf.Agent.PostInstall.PowerOff, conf.Agent.PostInstall.Reboot)
return nil
}
// Reset
if resetFlag {
log.Info("Resetting Elemental")
handleReset(client, osPlugin, hostname, conf.Registration.Token, conf.Agent.Reconciliation)
log.Info("Reset successful")
handlePost(osPlugin, conf.Agent.PostReset.PowerOff, conf.Agent.PostReset.Reboot)
return nil
}
// Normal reconcile
log.Info("Entering reconciliation loop")
for {
// Patch the host and receive the patched remote host back
log.Debug("Patching host")
host, err := client.PatchHost(api.HostPatchRequest{}, hostname)
if err != nil {
log.Error(err, "patching ElementalHost during normal reconcile")
log.Debugf("Waiting %s...", conf.Agent.Reconciliation.String())
time.Sleep(conf.Agent.Reconciliation)
continue
}
// Handle Reset trigger
//
// Reset should always be prioritized in the normal reconcile loop,
// to allow reset of machines that are otherwise stuck in other phases,
// like bootstrapping.
if host.NeedsReset {
log.Info("Triggering reset")
if err := osPlugin.TriggerReset(); err != nil {
log.Error(err, "triggering reset")
err := fmt.Errorf("triggering reset: %w", err)
attemptConditionReporting(client, hostname, clusterv1.Condition{
Type: infrastructurev1beta1.ResetReady,
Status: corev1.ConditionFalse,
Severity: clusterv1.ConditionSeverityError,
Reason: infrastructurev1beta1.ResetFailedReason,
Message: err.Error(),
})
continue
}
attemptConditionReporting(client, hostname, clusterv1.Condition{
Type: infrastructurev1beta1.ResetReady,
Status: corev1.ConditionFalse,
Severity: infrastructurev1beta1.WaitingForResetReasonSeverity,
Reason: infrastructurev1beta1.WaitingForResetReason,
Message: "Reset was triggered successfully. Waiting for host to reset.",
})
// If Reset was triggered successfully, exit the program.
log.Info("Reset was triggered successfully. Exiting program.")
return nil
}
// Handle bootstrap if needed
if host.BootstrapReady && !host.Bootstrapped {
log.Debug("Handling bootstrap application")
exit, err := handleBootstrap(fs, client, osPlugin, hostname)
if err != nil {
log.Error(err, "handling bootstrap")
attemptConditionReporting(client, hostname, clusterv1.Condition{
Type: infrastructurev1beta1.BootstrapReady,
Status: corev1.ConditionFalse,
Severity: clusterv1.ConditionSeverityError,
Reason: infrastructurev1beta1.BootstrapFailedReason,
Message: err.Error(),
})
}
if exit {
log.Info("Exiting program after bootstrap application.")
return nil
}
log.Debugf("Waiting %s...", conf.Agent.Reconciliation.String())
time.Sleep(conf.Agent.Reconciliation)
continue
}
log.Debugf("Waiting %s...", conf.Agent.Reconciliation.String())
time.Sleep(conf.Agent.Reconciliation)
}
},
}
//Define flags
cmd.PersistentFlags().BoolVar(&versionFlag, "version", false, "print version and exit")
cmd.PersistentFlags().BoolVar(&resetFlag, "reset", false, "reset the Elemental installation")
cmd.PersistentFlags().BoolVar(&installFlag, "install", false, "install Elemental")
cmd.PersistentFlags().BoolVar(®isterFlag, "register", false, "register Elemental host")
cmd.PersistentFlags().BoolVar(&debugFlag, "debug", false, "enable debug logging")
cmd.PersistentFlags().StringVar(&configPath, "config", configPathDefault, "agent config path")
return cmd
}
func getConfig(fs vfs.FS) (config.Config, error) {
conf := config.DefaultConfig()
// Use go-vfs afero compatibility layer (required by Viper)
afs := vfsafero.NewAferoFS(fs)
viper.SetFs(afs)
viper.SetConfigFile(configPath)
if err := viper.ReadInConfig(); err != nil {
return config.Config{}, fmt.Errorf("reading config: %w", err)
}
if err := viper.Unmarshal(&conf); err != nil {
return config.Config{}, fmt.Errorf("unmarshalling config: %w", err)
}
return conf, nil
}
func handleRegistration(client client.Client, osPlugin osplugin.Plugin, pubKey []byte, registrationToken string, registrationRecoveryPeriod time.Duration) (string, *api.RegistrationResponse) {
hostnameFormatter := hostname.NewFormatter(osPlugin)
var newHostname string
var registration *api.RegistrationResponse
var err error
registrationError := false
for {
// Wait for recovery
if registrationError {
log.Debugf("Waiting '%s' on registration error to recover", registrationRecoveryPeriod)
time.Sleep(registrationRecoveryPeriod)
}
// Fetch remote Registration
log.Debug("Fetching remote registration")
registration, err = client.GetRegistration(registrationToken)
if err != nil {
log.Error(err, "getting remote Registration")
registrationError = true
continue
}
// Pick a new hostname
// There is a tiny chance the random hostname generation will collide with existing ones.
// It's safer to generate a new one in case of host creation failure.
newHostname, err = hostnameFormatter.FormatHostname(registration.Config.Elemental.Agent.Hostname)
log.Debugf("Selected hostname: %s", newHostname)
if err != nil {
log.Error(err, "picking new hostname")
registrationError = true
continue
}
// Register new Elemental Host
log.Debugf("Registering new host: %s", newHostname)
if err := client.CreateHost(api.HostCreateRequest{
Name: newHostname,
Annotations: registration.HostAnnotations,
Labels: registration.HostLabels,
PubKey: string(pubKey),
}, registrationToken); err != nil {
log.Error(err, "registering new ElementalHost")
registrationError = true
continue
}
break
}
return newHostname, registration
}
func handlePostRegistration(osPlugin osplugin.Plugin, hostnameToSet string, id identity.Identity, registration *api.RegistrationResponse) error {
// Persist registered hostname
if err := osPlugin.InstallHostname(hostnameToSet); err != nil {
return fmt.Errorf("persisting hostname '%s': %w", hostnameToSet, err)
}
// Persist agent config
agentConfig := config.FromAPI(registration)
agentConfigBytes, err := yaml.Marshal(agentConfig)
if err != nil {
return fmt.Errorf("marshalling agent config: %w", err)
}
if err := osPlugin.InstallFile(agentConfigBytes, configPath, 0640, 0, 0); err != nil {
return fmt.Errorf("persisting agent config file '%s': %w", configPath, err)
}
// Persist identity file
identityBytes, err := id.Marshal()
if err != nil {
return fmt.Errorf("marshalling identity: %w", err)
}
privateKeyPath := fmt.Sprintf("%s/%s", agentConfig.Agent.WorkDir, identity.PrivateKeyFile)
if err := osPlugin.InstallFile(identityBytes, privateKeyPath, 0640, 0, 0); err != nil {
return fmt.Errorf("persisting private key file '%s': %w", privateKeyPath, err)
}
return nil
}
func handleInstall(client client.Client, osPlugin osplugin.Plugin, hostname string, registrationToken string, installationRecoveryPeriod time.Duration) {
cloudConfigAlreadyApplied := false
alreadyInstalled := false
var installationError error
installationErrorReason := infrastructurev1beta1.InstallationFailedReason
for {
if installationError != nil {
// Log error
log.Error(installationError, "installing host")
// Attempt to report failed condition on management server
attemptConditionReporting(client, hostname, clusterv1.Condition{
Type: infrastructurev1beta1.InstallationReady,
Status: corev1.ConditionFalse,
Severity: clusterv1.ConditionSeverityError,
Reason: installationErrorReason,
Message: installationError.Error(),
})
// Clear error for next attempt
installationError = nil
installationErrorReason = infrastructurev1beta1.InstallationFailedReason
// Wait for recovery (end user may fix the remote installation instructions meanwhile)
log.Debugf("Waiting '%s' on installation error for installation instructions to mutate", installationRecoveryPeriod)
time.Sleep(installationRecoveryPeriod)
}
// Fetch remote Registration
var registration *api.RegistrationResponse
var err error
if !cloudConfigAlreadyApplied || !alreadyInstalled {
log.Debug("Fetching remote registration")
registration, err = client.GetRegistration(registrationToken)
if err != nil {
installationError = fmt.Errorf("getting remote Registration: %w", err)
continue
}
}
// Apply Cloud Config
if !cloudConfigAlreadyApplied {
cloudConfigBytes, err := json.Marshal(registration.Config.CloudConfig)
if err != nil {
installationError = fmt.Errorf("marshalling cloud config: %w", err)
installationErrorReason = infrastructurev1beta1.CloudConfigInstallationFailedReason
continue
}
if err := osPlugin.InstallCloudInit(cloudConfigBytes); err != nil {
installationError = fmt.Errorf("installing cloud config: %w", err)
installationErrorReason = infrastructurev1beta1.CloudConfigInstallationFailedReason
continue
}
cloudConfigAlreadyApplied = true
}
// Install
if !alreadyInstalled {
installBytes, err := json.Marshal(registration.Config.Elemental.Install)
if err != nil {
installationError = fmt.Errorf("marshalling install config: %w", err)
continue
}
if err := osPlugin.Install(installBytes); err != nil {
installationError = fmt.Errorf("installing host: %w", err)
continue
}
alreadyInstalled = true
}
// Report installation success
patchRequest := api.HostPatchRequest{Installed: ptr.To(true)}
patchRequest.SetCondition(infrastructurev1beta1.InstallationReady,
corev1.ConditionTrue,
clusterv1.ConditionSeverityInfo,
"", "")
if _, err := client.PatchHost(patchRequest, hostname); err != nil {
installationError = fmt.Errorf("patching host with installation successful: %w", err)
continue
}
break
}
}
func handleReset(client client.Client, osPlugin osplugin.Plugin, hostname string, registrationToken string, resetRecoveryPeriod time.Duration) {
var resetError error
alreadyReset := false
for {
// Wait for recovery (end user may fix the remote reset instructions meanwhile)
if resetError != nil {
// Log error
log.Error(resetError, "resetting")
// Attempt to report failed condition on management server
attemptConditionReporting(client, hostname, clusterv1.Condition{
Type: infrastructurev1beta1.ResetReady,
Status: corev1.ConditionFalse,
Severity: clusterv1.ConditionSeverityError,
Reason: infrastructurev1beta1.ResetFailedReason,
Message: resetError.Error(),
})
// Clear error for next attempt
resetError = nil
log.Debugf("Waiting '%s' on reset error for reset instructions to mutate", resetRecoveryPeriod)
time.Sleep(resetRecoveryPeriod)
}
// Mark ElementalHost for deletion
// Repeat in case of failures. May be exploited server side to track repeated attempts.
log.Debugf("Marking ElementalHost for deletion: %s", hostname)
if err := client.DeleteHost(hostname); err != nil {
resetError = fmt.Errorf("marking host for deletion: %w", err)
continue
}
// Reset
if !alreadyReset {
// Fetch remote Registration
log.Debug("Fetching remote registration")
registration, err := client.GetRegistration(registrationToken)
if err != nil {
resetError = fmt.Errorf("getting remote Registration: %w", err)
continue
}
log.Debug("Resetting...")
resetBytes, err := json.Marshal(registration.Config.Elemental.Reset)
if err != nil {
resetError = fmt.Errorf("marshalling reset config: %w", err)
continue
}
if err := osPlugin.Reset(resetBytes); err != nil {
resetError = fmt.Errorf("resetting host: %w", err)
continue
}
alreadyReset = true
}
// Report reset success
log.Debug("Patching ElementalHost as reset")
patchRequest := api.HostPatchRequest{Reset: ptr.To(true)}
patchRequest.SetCondition(infrastructurev1beta1.ResetReady,
corev1.ConditionTrue,
clusterv1.ConditionSeverityInfo,
"", "")
if _, err := client.PatchHost(patchRequest, hostname); err != nil {
resetError = fmt.Errorf("patching host with reset successful: %w", err)
continue
}
break
}
}
// handleBootstrap is usually called twice during the bootstrap phase.
//
// The first call should normally fetch the remote bootstrap config and propagate it to the plugin implementation.
// The system should then reboot, and upon successful reboot, the `/run/cluster-api/bootstrap-success.complete`
// sentinel file is expected to exist.
// Note that the reboot is currently enforced, since both `cloud-init` and `ignition` formats are meant to be applied
// during system boot.
// See: https://cluster-api.sigs.k8s.io/developer/providers/bootstrap.html#sentinel-file
//
// The second call should normally patch the remote Host resource as bootstrapped,
// after verifying the existance of `/run/cluster-api/bootstrap-success.complete`.
// Note that since `/run` is normally mounted as tmpfs and the bootstrap config is not re-executed at every boot,
// the remote host needs to be patched before the system is ever rebooted an additional time.
// If reboot happens and `/run/cluster-api/bootstrap-success.complete` is not found on the already-bootstrapped system,
// the plugin will be invoked again to re-apply the bootstrap config. It's up to the plugin implementation to recover
// from this state if possible, or to just return an error to highlight manual intervention is needed (and possibly a machine reset).
func handleBootstrap(fs vfs.FS, client client.Client, osPlugin osplugin.Plugin, hostname string) (bool, error) {
// Assume system was already bootstrapped if sentinel file is found
_, err := fs.Stat(bootstrapSentinelFile)
if err == nil {
// Patch the ElementalHost as successfully bootstrapped
patchRequest := api.HostPatchRequest{Bootstrapped: ptr.To(true)}
patchRequest.SetCondition(infrastructurev1beta1.BootstrapReady,
corev1.ConditionTrue,
clusterv1.ConditionSeverityInfo,
"", "")
if _, err := client.PatchHost(patchRequest, hostname); err != nil {
return false, fmt.Errorf("patching ElementalHost after bootstrap: %w", err)
}
log.Info("Bootstrap config applied successfully")
return false, nil
}
// Sentinel file not found, assume system needs bootstrapping
if os.IsNotExist(err) {
log.Debug("Fetching bootstrap config")
bootstrap, err := client.GetBootstrap(hostname)
if err != nil {
return false, fmt.Errorf("fetching bootstrap config: %w", err)
}
log.Info("Applying bootstrap config")
if err := osPlugin.Bootstrap(bootstrap.Format, []byte(bootstrap.Config)); err != nil {
return false, fmt.Errorf("applying bootstrap config: %w", err)
}
attemptConditionReporting(client, hostname, clusterv1.Condition{
Type: infrastructurev1beta1.BootstrapReady,
Status: corev1.ConditionFalse,
Severity: infrastructurev1beta1.WaitingForBootstrapReasonSeverity,
Reason: infrastructurev1beta1.WaitingForBootstrapReason,
Message: "Waiting for bootstrap to be executed",
})
log.Info("System is rebooting to execute the bootstrap configuration...")
if err := osPlugin.Reboot(); err != nil {
// Exit the program in case of reboot failures
// Assume this is not recoverable and requires manual intervention
return true, fmt.Errorf("rebooting system for bootstrapping: %w", err)
}
return true, nil
}
return false, fmt.Errorf("verifying bootstrap sentinel file '%s': %w", bootstrapSentinelFile, err)
}
func handlePost(osPlugin osplugin.Plugin, poweroff bool, reboot bool) {
if poweroff {
log.Info("Powering off system")
if err := osPlugin.PowerOff(); err != nil {
log.Error(err, "Powering off system")
}
} else if reboot {
log.Info("Rebooting system")
if err := osPlugin.Reboot(); err != nil {
log.Error(err, "Rebooting system")
}
}
}
// attemptConditionReporting is a best effort method to update the remote condition.
// Due to the unexpected nature of failures, we should not attempt indefinitely as there is no indication for recovery.
// For example if a network error occurs, leading to a failed condition, it's likely that reporting the condition will fail as well.
// The controller should always try to reconcile the 'True' status for each Host condition, so reporting failures should not be critical.
func attemptConditionReporting(client client.Client, hostname string, condition clusterv1.Condition) {
if _, err := client.PatchHost(api.HostPatchRequest{
Condition: &condition,
}, hostname); err != nil {
log.Error(err, "reporting condition", "conditionType", condition.Type, "conditionReason", condition.Reason)
}
}