github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/providers/registration/file_content.go (about)

     1  // Copyright (c) 2021, R.I. Pienaar and the Choria Project contributors
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package registration
     6  
     7  import (
     8  	"context"
     9  	"encoding/json"
    10  	"fmt"
    11  	"math/rand"
    12  	"os"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/choria-io/go-choria/config"
    17  	"github.com/choria-io/go-choria/internal/util"
    18  	"github.com/choria-io/go-choria/server/data"
    19  
    20  	"github.com/sirupsen/logrus"
    21  )
    22  
    23  // FileContent is a fully managed registration plugin for the choria server instance
    24  // it reads a file and publishing it to the collective regularly
    25  type FileContent struct {
    26  	dataFile string
    27  	c        *config.Config
    28  	log      *logrus.Entry
    29  
    30  	prevMtime int64
    31  }
    32  
    33  // FileContentMessage contains message being published
    34  type FileContentMessage struct {
    35  	Mtime    int64  `json:"mtime"`
    36  	File     string `json:"file"`
    37  	Updated  bool   `json:"updated"`
    38  	Protocol string `json:"protocol"`
    39  	Content  []byte `json:"content,omitempty"`
    40  	ZContent []byte `json:"zcontent,omitempty"`
    41  }
    42  
    43  // NewFileContent creates a new fully managed registration plugin instance
    44  func NewFileContent(c *config.Config, _ ServerInfoSource, logger *logrus.Entry) (*FileContent, error) {
    45  	if c.Choria.FileContentRegistrationData == "" {
    46  		return nil, fmt.Errorf("file fontent registration is enabled but no source data is configured, please set plugin.choria.registration.file_content.data")
    47  	}
    48  
    49  	reg := &FileContent{}
    50  	reg.Init(c, logger)
    51  
    52  	return reg, nil
    53  }
    54  
    55  // Init sets up the plugin
    56  func (fc *FileContent) Init(c *config.Config, logger *logrus.Entry) {
    57  	fc.c = c
    58  	fc.dataFile = c.Choria.FileContentRegistrationData
    59  	fc.log = logger.WithFields(logrus.Fields{"registration": "file_content", "source": fc.dataFile})
    60  
    61  	fc.log.Infof("Configured File Content Registration with source '%s' and target '%s'", fc.dataFile, c.Choria.FileContentRegistrationTarget)
    62  }
    63  
    64  // StartRegistration starts stats a publishing loop
    65  func (fc *FileContent) StartRegistration(ctx context.Context, wg *sync.WaitGroup, interval int, output chan *data.RegistrationItem) {
    66  	defer wg.Done()
    67  
    68  	delay := time.Duration(rand.Intn(4)+1) * time.Second
    69  	fc.log.Infof("Sleeping %v before first registration publish", delay)
    70  	err := util.InterruptibleSleep(ctx, delay)
    71  	if err != nil {
    72  		return
    73  	}
    74  
    75  	err = fc.publish(output)
    76  	if err != nil {
    77  		fc.log.Errorf("Could not create registration data: %s", err)
    78  	}
    79  
    80  	ticker := time.NewTicker(time.Duration(interval) * time.Second)
    81  
    82  	for {
    83  		select {
    84  		case <-ticker.C:
    85  			err = fc.publish(output)
    86  			if err != nil {
    87  				fc.log.Errorf("Could not create registration data: %s", err)
    88  			}
    89  
    90  		case <-ctx.Done():
    91  			return
    92  		}
    93  	}
    94  }
    95  
    96  func (fc *FileContent) publish(output chan *data.RegistrationItem) error {
    97  	fc.log.Infof("Starting file_content registration poll")
    98  
    99  	fstat, err := os.Stat(fc.dataFile)
   100  	if os.IsNotExist(err) {
   101  		return fmt.Errorf("could not find data file %s", fc.dataFile)
   102  	}
   103  
   104  	if fstat.Size() == 0 {
   105  		return fmt.Errorf("data file %s is empty", fc.dataFile)
   106  	}
   107  
   108  	fstat, err = os.Stat(fc.dataFile)
   109  	if err != nil {
   110  		return fmt.Errorf("could not obtain file times: %s", err)
   111  	}
   112  
   113  	dat, err := os.ReadFile(fc.dataFile)
   114  	if err != nil {
   115  		return fmt.Errorf("could not read file registration source %s: %s", fc.dataFile, err)
   116  	}
   117  
   118  	msg := &FileContentMessage{
   119  		Protocol: "choria:registration:filecontent:1",
   120  		File:     fc.dataFile,
   121  		Mtime:    fstat.ModTime().Unix(),
   122  	}
   123  
   124  	// the first time it starts we just have no idea, so we set it to whatever
   125  	// it is now which would also avoid setting updated=true, we do not want a
   126  	// large fleet restart to mass trigger a needless full site replication
   127  	if fc.prevMtime == 0 {
   128  		fc.prevMtime = msg.Mtime
   129  	}
   130  
   131  	if msg.Mtime > fc.prevMtime {
   132  		msg.Updated = true
   133  		fc.prevMtime = msg.Mtime
   134  	}
   135  
   136  	if fc.c.Choria.FileContentCompression {
   137  		zdat, err := compress(dat)
   138  		if err != nil {
   139  			fc.log.Warnf("Could not compress file registration data: %s", err)
   140  		} else {
   141  			msg.ZContent = zdat
   142  		}
   143  	}
   144  
   145  	if msg.ZContent == nil {
   146  		msg.Content = dat
   147  	}
   148  
   149  	jdat, err := json.Marshal(msg)
   150  	if err != nil {
   151  		return fmt.Errorf("could not json marshal registration message: %s", err)
   152  	}
   153  
   154  	item := &data.RegistrationItem{
   155  		Data:        jdat,
   156  		Destination: fc.c.Choria.FileContentRegistrationTarget,
   157  	}
   158  
   159  	if item.Destination == "" {
   160  		item.TargetAgent = "registration"
   161  	}
   162  
   163  	output <- item
   164  
   165  	return nil
   166  }