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 }