github.com/Financial-Times/publish-availability-monitor@v1.12.0/envs/file_environments.go (about)

     1  package envs
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/md5"
     6  	"encoding/hex"
     7  	"encoding/json"
     8  	"fmt"
     9  	"io"
    10  	"net/url"
    11  	"os"
    12  	"strings"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/Financial-Times/go-logger/v2"
    17  	"github.com/Financial-Times/publish-availability-monitor/config"
    18  	"github.com/Financial-Times/publish-availability-monitor/feeds"
    19  )
    20  
    21  var validatorCredentials string
    22  
    23  type Credentials struct {
    24  	EnvName  string `json:"env-name"`
    25  	Username string `json:"username"`
    26  	Password string `json:"password"`
    27  }
    28  
    29  func WatchConfigFiles(
    30  	wg *sync.WaitGroup,
    31  	envsFileName, envCredentialsFileName, validationCredentialsFileName string,
    32  	configRefreshPeriod int,
    33  	configFilesHashValues map[string]string,
    34  	environments *Environments,
    35  	subscribedFeeds map[string][]feeds.Feed,
    36  	appConfig *config.AppConfig,
    37  	log *logger.UPPLogger,
    38  ) {
    39  	ticker := newTicker(0, time.Minute*time.Duration(configRefreshPeriod))
    40  	first := true
    41  	defer func() {
    42  		markWaitGroupDone(wg, first)
    43  	}()
    44  
    45  	for range ticker.C {
    46  		err := updateEnvsIfChanged(envsFileName, envCredentialsFileName, configFilesHashValues, environments, subscribedFeeds, appConfig, log)
    47  		if err != nil {
    48  			log.WithError(err).Errorf("Could not update envs config")
    49  		}
    50  
    51  		err = updateValidationCredentialsIfChanged(validationCredentialsFileName, configFilesHashValues, log)
    52  		if err != nil {
    53  			log.WithError(err).Errorf("Could not update validation credentials config")
    54  		}
    55  
    56  		first = markWaitGroupDone(wg, first)
    57  	}
    58  }
    59  
    60  func markWaitGroupDone(wg *sync.WaitGroup, first bool) bool {
    61  	if first {
    62  		wg.Done()
    63  		first = false
    64  	}
    65  
    66  	return first
    67  }
    68  
    69  func newTicker(delay, repeat time.Duration) *time.Ticker {
    70  	// adapted from https://stackoverflow.com/questions/32705582/how-to-get-time-tick-to-tick-immediately
    71  	ticker := time.NewTicker(repeat)
    72  	oc := ticker.C
    73  	nc := make(chan time.Time, 1)
    74  	go func() {
    75  		time.Sleep(delay)
    76  		nc <- time.Now()
    77  		for tm := range oc {
    78  			nc <- tm
    79  		}
    80  	}()
    81  	ticker.C = nc
    82  	return ticker
    83  }
    84  
    85  func updateValidationCredentialsIfChanged(validationCredentialsFileName string, configFilesHashValues map[string]string, log *logger.UPPLogger) error {
    86  	fileContents, err := os.ReadFile(validationCredentialsFileName)
    87  	if err != nil {
    88  		return fmt.Errorf("could not read creds file [%v] because [%s]", validationCredentialsFileName, err)
    89  	}
    90  
    91  	var validationCredentialsChanged bool
    92  	var credsNewHash string
    93  	if validationCredentialsChanged, credsNewHash, err = isFileChanged(fileContents, validationCredentialsFileName, configFilesHashValues); err != nil {
    94  		return fmt.Errorf("could not detect if creds file [%s] was changed because: [%s]", validationCredentialsFileName, err)
    95  	}
    96  
    97  	if !validationCredentialsChanged {
    98  		return nil
    99  	}
   100  
   101  	err = updateValidationCredentials(fileContents, log)
   102  	if err != nil {
   103  		return fmt.Errorf("cannot update validation credentials because [%s]", err)
   104  	}
   105  
   106  	configFilesHashValues[validationCredentialsFileName] = credsNewHash
   107  	return nil
   108  }
   109  
   110  func updateEnvsIfChanged(
   111  	envsFileName, envCredentialsFileName string,
   112  	configFilesHashValues map[string]string,
   113  	environments *Environments,
   114  	subscribedFeeds map[string][]feeds.Feed,
   115  	appConfig *config.AppConfig,
   116  	log *logger.UPPLogger,
   117  ) error {
   118  	var envsFileChanged, envCredentialsChanged bool
   119  	var envsNewHash, credsNewHash string
   120  
   121  	envsfileContents, err := os.ReadFile(envsFileName)
   122  	if err != nil {
   123  		return fmt.Errorf("could not read envs file [%s] because [%s]", envsFileName, err)
   124  	}
   125  
   126  	if envsFileChanged, envsNewHash, err = isFileChanged(envsfileContents, envsFileName, configFilesHashValues); err != nil {
   127  		return fmt.Errorf("could not detect if envs file [%s] was changed because [%s]", envsFileName, err)
   128  	}
   129  
   130  	credsFileContents, err := os.ReadFile(envCredentialsFileName)
   131  	if err != nil {
   132  		return fmt.Errorf("could not read creds file [%s] because [%s]", envCredentialsFileName, err)
   133  	}
   134  
   135  	if envCredentialsChanged, credsNewHash, err = isFileChanged(credsFileContents, envCredentialsFileName, configFilesHashValues); err != nil {
   136  		return fmt.Errorf("could not detect if credentials file [%s] was changed because [%s]", envCredentialsFileName, err)
   137  	}
   138  
   139  	if !envsFileChanged && !envCredentialsChanged {
   140  		return nil
   141  	}
   142  
   143  	err = updateEnvs(envsfileContents, credsFileContents, environments, subscribedFeeds, appConfig, log)
   144  	if err != nil {
   145  		return fmt.Errorf("cannot update environments and credentials because [%s]", err)
   146  	}
   147  	configFilesHashValues[envsFileName] = envsNewHash
   148  	configFilesHashValues[envCredentialsFileName] = credsNewHash
   149  	return nil
   150  }
   151  
   152  func isFileChanged(contents []byte, fileName string, configFilesHashValues map[string]string) (bool, string, error) {
   153  	currentHash, err := computeMD5Hash(contents)
   154  	if err != nil {
   155  		return false, "", fmt.Errorf("could not compute hash value for file [%s] because [%s]", fileName, err)
   156  	}
   157  
   158  	previousHash, found := configFilesHashValues[fileName]
   159  	if found && previousHash == currentHash {
   160  		return false, previousHash, nil
   161  	}
   162  
   163  	return true, currentHash, nil
   164  }
   165  
   166  func computeMD5Hash(data []byte) (string, error) {
   167  	hash := md5.New()
   168  	if _, err := io.Copy(hash, bytes.NewReader(data)); err != nil {
   169  		return "", fmt.Errorf("could not compute hash value because [%s]", err)
   170  	}
   171  	hashValue := hash.Sum(nil)[:16]
   172  	return hex.EncodeToString(hashValue), nil
   173  }
   174  
   175  func updateEnvs(envsFileData []byte, credsFileData []byte, environments *Environments, subscribedFeeds map[string][]feeds.Feed, appConfig *config.AppConfig, log *logger.UPPLogger) error {
   176  	log.Infof("Env config files changed. Updating envs")
   177  
   178  	jsonParser := json.NewDecoder(bytes.NewReader(envsFileData))
   179  	envsFromFile := []Environment{}
   180  	err := jsonParser.Decode(&envsFromFile)
   181  	if err != nil {
   182  		return fmt.Errorf("cannot parse environmente because [%s]", err)
   183  	}
   184  
   185  	validEnvs := filterInvalidEnvs(envsFromFile, log)
   186  
   187  	jsonParser = json.NewDecoder(bytes.NewReader(credsFileData))
   188  	envCredentials := []Credentials{}
   189  	err = jsonParser.Decode(&envCredentials)
   190  
   191  	if err != nil {
   192  		return fmt.Errorf("cannot parse credentials because [%s]", err)
   193  	}
   194  
   195  	removedEnvs := parseEnvsIntoMap(validEnvs, envCredentials, environments, log)
   196  	configureFileFeeds(environments.Values(), removedEnvs, subscribedFeeds, appConfig, log)
   197  	environments.SetReady(true)
   198  
   199  	return nil
   200  }
   201  
   202  func updateValidationCredentials(data []byte, log *logger.UPPLogger) error {
   203  	log.Info("Updating validation credentials")
   204  
   205  	jsonParser := json.NewDecoder(bytes.NewReader(data))
   206  	credentials := Credentials{}
   207  	err := jsonParser.Decode(&credentials)
   208  	if err != nil {
   209  		return err
   210  	}
   211  	validatorCredentials = credentials.Username + ":" + credentials.Password
   212  	return nil
   213  }
   214  
   215  //nolint:gocognit
   216  func configureFileFeeds(envs []Environment, removedEnvs []string, subscribedFeeds map[string][]feeds.Feed, appConfig *config.AppConfig, log *logger.UPPLogger) {
   217  	for _, envName := range removedEnvs {
   218  		feeds, found := subscribedFeeds[envName]
   219  		if found {
   220  			for _, f := range feeds {
   221  				f.Stop()
   222  			}
   223  		}
   224  
   225  		delete(subscribedFeeds, envName)
   226  	}
   227  
   228  	for _, metric := range appConfig.MetricConf {
   229  		for _, env := range envs {
   230  			var envFeeds []feeds.Feed
   231  			var found bool
   232  			if envFeeds, found = subscribedFeeds[env.Name]; !found {
   233  				envFeeds = make([]feeds.Feed, 0)
   234  			}
   235  
   236  			found = false
   237  			for _, f := range envFeeds {
   238  				if f.FeedName() == metric.Alias {
   239  					f.SetCredentials(env.Username, env.Password)
   240  					found = true
   241  					break
   242  				}
   243  			}
   244  
   245  			if !found {
   246  				endpointURL, err := url.Parse(env.ReadURL + metric.Endpoint)
   247  				if err != nil {
   248  					log.WithError(err).Errorf("Cannot parse url [%v]", metric.Endpoint)
   249  					continue
   250  				}
   251  
   252  				interval := appConfig.Threshold / metric.Granularity
   253  
   254  				if f := feeds.NewNotificationsFeed(metric.Alias, *endpointURL, appConfig.Threshold, interval, env.Username, env.Password, metric.APIKey, log); f != nil {
   255  					subscribedFeeds[env.Name] = append(envFeeds, f)
   256  					f.Start()
   257  				}
   258  			}
   259  		}
   260  	}
   261  }
   262  
   263  func filterInvalidEnvs(envsFromFile []Environment, log *logger.UPPLogger) []Environment {
   264  	var validEnvs []Environment
   265  	for _, env := range envsFromFile {
   266  		//envs without name are invalid
   267  		if env.Name == "" {
   268  			log.Errorf("Env %v has an empty name, skipping it", env)
   269  			continue
   270  		}
   271  
   272  		//envs without read-url are invalid
   273  		if env.ReadURL == "" {
   274  			log.Errorf("Env with name %s does not have readUrl, skipping it", env.Name)
   275  			continue
   276  		}
   277  
   278  		validEnvs = append(validEnvs, env)
   279  	}
   280  
   281  	return validEnvs
   282  }
   283  
   284  func parseEnvsIntoMap(envs []Environment, envCredentials []Credentials, environments *Environments, log *logger.UPPLogger) []string {
   285  	//enhance envs with credentials
   286  	for i, env := range envs {
   287  		for _, envCredentials := range envCredentials {
   288  			if env.Name == envCredentials.EnvName {
   289  				envs[i].Username = envCredentials.Username
   290  				envs[i].Password = envCredentials.Password
   291  				break
   292  			}
   293  		}
   294  
   295  		if envs[i].Username == "" || envs[i].Password == "" {
   296  			log.Infof("No credentials provided for env with name %s", env.Name)
   297  		}
   298  	}
   299  
   300  	//remove envs that don't exist anymore
   301  	removedEnvs := make([]string, 0)
   302  	envNames := environments.Names()
   303  	for _, envName := range envNames {
   304  		if !isEnvInSlice(envName, envs) {
   305  			log.Infof("removing environment from monitoring: %v", envName)
   306  			environments.RemoveEnvironment(envName)
   307  			removedEnvs = append(removedEnvs, envName)
   308  		}
   309  	}
   310  
   311  	//update envs
   312  	for _, env := range envs {
   313  		envName := env.Name
   314  		environments.SetEnvironment(envName, env)
   315  		log.Infof("Added environment to monitoring: %s", envName)
   316  	}
   317  
   318  	return removedEnvs
   319  }
   320  
   321  func isEnvInSlice(envName string, envs []Environment) bool {
   322  	for _, env := range envs {
   323  		if env.Name == envName {
   324  			return true
   325  		}
   326  	}
   327  
   328  	return false
   329  }
   330  
   331  func GetValidationCredentials() (string, string) {
   332  	if strings.Contains(validatorCredentials, ":") {
   333  		unpw := strings.SplitN(validatorCredentials, ":", 2)
   334  		return unpw[0], unpw[1]
   335  	}
   336  
   337  	return "", ""
   338  }