package displayctl import ( "context" "fmt" "sync" "time" "github.com/go-logr/logr" kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" v2 "edge-infra.dev/pkg/sds/display/k8s/apis/v2" ) const ( annotationPatchFmt = `{"metadata":{"annotations":{"%s":"%s"}}}` tick = time.Millisecond * 100 ) // nodeDisplayConfigAnnotator provides methods for annotating // the host NodeDisplayConfig. // // Annotations can be patched immediately, or rate-limited to // avoid multiple patches to the same annotation in a short // amount of time. type nodeDisplayConfigAnnotator struct { Hostname string Annotation string Client client.Client ErrChan chan error // logger used to log message on annotation log logr.Logger // log message to show after annotating logMessage string // used to cancel the running annotator thread cancel context.CancelFunc mu sync.Mutex } func newNodeDisplayConfigAnnotator(hostname, annotation string, c client.Client, log logr.Logger, logMessage string) *nodeDisplayConfigAnnotator { return &nodeDisplayConfigAnnotator{ Hostname: hostname, Annotation: annotation, Client: c, ErrChan: make(chan error), log: log, logMessage: logMessage, } } // Patches the host's NodeDisplayConfig annotation with the value, // with rate-limiting to avoid multiple patches of the same annotation // in a short amount of time. // // Asynchronously applies the patch after the timeout is reached. If // AnnotateRateLimited is called again before the timeout is reached, // the previous call will be cancelled and the timeout started again. // If the context becomes cancelled, the NodeDisplayConfig will not // be patched. func (a *nodeDisplayConfigAnnotator) AnnotateRateLimited(ctx context.Context, value string, timeout time.Duration) { // cancel running annotator thread if a.cancel != nil { a.cancel() } a.mu.Lock() ctx, cancel := context.WithCancel(ctx) a.cancel = cancel // asynchronously annotate after timeout is reached, doing // nothing if the context is cancelled first go a.annotateAfterTimeoutOrCancel(ctx, value, timeout) } // Annotates the NodeDisplayConfig after the timeout is reached. // Exits without annotating if the context is canceled first. func (a *nodeDisplayConfigAnnotator) annotateAfterTimeoutOrCancel(ctx context.Context, value string, timeout time.Duration) { defer a.mu.Unlock() select { case <-time.After(timeout): if err := a.Annotate(ctx, value); err != nil { a.ErrChan <- err } case <-ctx.Done(): return } } // Patches the host's NodeDisplayConfig annotation with the value. // // Does nothing if the NodeDisplayConfig cannot be found. func (a *nodeDisplayConfigAnnotator) Annotate(ctx context.Context, value string) error { key := client.ObjectKey{ Name: a.Hostname, } nodeDisplayConfig := &v2.NodeDisplayConfig{} if err := a.Client.Get(context.Background(), key, nodeDisplayConfig); kerrors.IsNotFound(err) { return nil } else if err != nil { return fmt.Errorf("unable to get %s NodeDisplayConfig: %w", a.Hostname, err) } patch := client.RawPatch(types.MergePatchType, []byte(fmt.Sprintf(annotationPatchFmt, a.Annotation, value))) if err := a.Client.Patch(ctx, nodeDisplayConfig, patch); err != nil { return fmt.Errorf("unable patch %s annotation for %s NodeDisplayConfig: %w", a.Annotation, a.Hostname, err) } if a.logMessage != "" { a.log.Info(a.logMessage) } return nil }