github.com/google/cloudprober@v0.11.3/rds/file/file.go (about) 1 // Copyright 2021 The Cloudprober Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 /* 16 Package file implements a file-based targets provider for cloudprober. 17 */ 18 package file 19 20 import ( 21 "fmt" 22 "math/rand" 23 "path/filepath" 24 "sync" 25 "time" 26 27 "github.com/google/cloudprober/common/file" 28 "github.com/google/cloudprober/logger" 29 configpb "github.com/google/cloudprober/rds/file/proto" 30 pb "github.com/google/cloudprober/rds/proto" 31 "github.com/google/cloudprober/rds/server/filter" 32 "google.golang.org/protobuf/encoding/protojson" 33 "google.golang.org/protobuf/encoding/prototext" 34 "google.golang.org/protobuf/proto" 35 ) 36 37 // DefaultProviderID is the povider id to use for this provider if a provider 38 // id is not configured explicitly. 39 const DefaultProviderID = "file" 40 41 /* 42 SupportedFilters defines filters supported by the file-based resources 43 type. 44 Example: 45 filter { 46 key: "name" 47 value: "cloudprober.*" 48 } 49 filter { 50 key: "labels.app" 51 value: "service-a" 52 } 53 */ 54 var SupportedFilters = struct { 55 RegexFilterKeys []string 56 LabelsFilter bool 57 }{ 58 []string{"name"}, 59 true, 60 } 61 62 // lister implements file-based targets lister. 63 type lister struct { 64 mu sync.RWMutex 65 filePath string 66 format configpb.ProviderConfig_Format 67 resources []*pb.Resource 68 l *logger.Logger 69 70 lastUpdated time.Time 71 checkModTime bool 72 } 73 74 func (ls *lister) lastModified() int64 { 75 ls.mu.RLock() 76 defer ls.mu.RUnlock() 77 return ls.lastUpdated.Unix() 78 } 79 80 // listResources returns the last successfully parsed list of resources. 81 func (ls *lister) listResources(req *pb.ListResourcesRequest) (*pb.ListResourcesResponse, error) { 82 ls.mu.RLock() 83 defer ls.mu.RUnlock() 84 85 // If there are no filters, return early. 86 if len(req.GetFilter()) == 0 { 87 return &pb.ListResourcesResponse{ 88 Resources: append([]*pb.Resource{}, ls.resources...), 89 LastModified: proto.Int64(ls.lastUpdated.Unix()), 90 }, nil 91 } 92 93 allFilters, err := filter.ParseFilters(req.GetFilter(), SupportedFilters.RegexFilterKeys, "") 94 if err != nil { 95 return nil, err 96 } 97 nameFilter, labelsFilter := allFilters.RegexFilters["name"], allFilters.LabelsFilter 98 99 // Allocate resources for response early but optimize for large number of 100 // total resources. 101 allocSize := len(ls.resources) 102 if allocSize > 100 { 103 allocSize = 100 104 } 105 resources := make([]*pb.Resource, 0, allocSize) 106 107 for _, res := range ls.resources { 108 if nameFilter != nil && !nameFilter.Match(res.GetName(), ls.l) { 109 continue 110 } 111 if labelsFilter != nil && !labelsFilter.Match(res.GetLabels(), ls.l) { 112 continue 113 } 114 resources = append(resources, res) 115 } 116 117 ls.l.Infof("file.ListResources: returning %d resources out of %d", len(resources), len(ls.resources)) 118 return &pb.ListResourcesResponse{ 119 Resources: resources, 120 LastModified: proto.Int64(ls.lastUpdated.Unix()), 121 }, nil 122 } 123 124 func (ls *lister) parseFileContent(b []byte) ([]*pb.Resource, error) { 125 resources := &configpb.FileResources{} 126 127 switch ls.format { 128 case configpb.ProviderConfig_TEXTPB: 129 err := prototext.Unmarshal(b, resources) 130 if err != nil { 131 return nil, fmt.Errorf("file_provider(%s): error unmarshaling as text proto: %v", ls.filePath, err) 132 } 133 return resources.GetResource(), nil 134 case configpb.ProviderConfig_JSON: 135 err := protojson.Unmarshal(b, resources) 136 if err != nil { 137 return nil, fmt.Errorf("file_provider(%s): error unmarshaling as JSON: %v", ls.filePath, err) 138 } 139 return resources.GetResource(), nil 140 } 141 142 return nil, fmt.Errorf("file_provider(%s): unknown format - %v", ls.filePath, ls.format) 143 } 144 145 func (ls *lister) shouldReloadFile() bool { 146 if !ls.checkModTime { 147 return true 148 } 149 150 modTime, err := file.ModTime(ls.filePath) 151 if err != nil { 152 ls.l.Warningf("file(%s): Error getting modified time: %v; Ignoring modified time check.", ls.filePath, err) 153 return true 154 } 155 156 ls.mu.RLock() 157 defer ls.mu.RUnlock() 158 return modTime.After(ls.lastUpdated) 159 } 160 161 func (ls *lister) refresh() error { 162 if !ls.shouldReloadFile() { 163 ls.l.Infof("file(%s): Skipping reloading file as it has not changed since its last refresh at %v", ls.filePath, ls.lastUpdated) 164 return nil 165 } 166 167 b, err := file.ReadFile(ls.filePath) 168 if err != nil { 169 return fmt.Errorf("file(%s): error while reading file: %v", ls.filePath, err) 170 } 171 172 resources, err := ls.parseFileContent(b) 173 if err != nil { 174 return err 175 } 176 177 ls.mu.Lock() 178 defer ls.mu.Unlock() 179 180 ls.lastUpdated = time.Now() 181 ls.resources = resources 182 183 ls.l.Infof("file_provider(%s): Read %d resources.", ls.filePath, len(ls.resources)) 184 return nil 185 } 186 187 func formatFromPath(path string) configpb.ProviderConfig_Format { 188 switch filepath.Ext(path) { 189 case ".textpb": 190 return configpb.ProviderConfig_TEXTPB 191 case ".json": 192 return configpb.ProviderConfig_JSON 193 } 194 return configpb.ProviderConfig_TEXTPB 195 } 196 197 // newLister creates a new file-based targets lister. 198 func newLister(filePath string, c *configpb.ProviderConfig, l *logger.Logger) (*lister, error) { 199 format := c.GetFormat() 200 if format == configpb.ProviderConfig_UNSPECIFIED { 201 format = formatFromPath(filePath) 202 l.Infof("file_provider: Determined file format from file name: %v", format) 203 } 204 205 ls := &lister{ 206 filePath: filePath, 207 format: format, 208 l: l, 209 checkModTime: !c.GetDisableModifiedTimeCheck(), 210 } 211 212 reEvalSec := c.GetReEvalSec() 213 if reEvalSec == 0 { 214 return ls, ls.refresh() 215 } 216 217 reEvalInterval := time.Duration(reEvalSec) * time.Second 218 go func() { 219 if err := ls.refresh(); err != nil { 220 l.Error(err.Error()) 221 } 222 // Introduce a random delay between 0-reEvalInterval before 223 // starting the refresh loop. If there are multiple cloudprober 224 // instances, this will make sure that each instance refreshes 225 // at a different point of time. 226 rand.Seed(time.Now().UnixNano()) 227 randomDelaySec := rand.Intn(int(reEvalInterval.Seconds())) 228 time.Sleep(time.Duration(randomDelaySec) * time.Second) 229 for range time.Tick(reEvalInterval) { 230 if err := ls.refresh(); err != nil { 231 l.Error(err.Error()) 232 } 233 } 234 }() 235 236 return ls, nil 237 } 238 239 func responseWithCacheCheck(ls *lister, req *pb.ListResourcesRequest) (*pb.ListResourcesResponse, error) { 240 if req.GetIfModifiedSince() == 0 { 241 return ls.listResources(req) 242 } 243 244 if lastModified := ls.lastModified(); lastModified <= req.GetIfModifiedSince() { 245 return &pb.ListResourcesResponse{ 246 LastModified: proto.Int64(lastModified), 247 }, nil 248 } 249 250 return ls.listResources(req) 251 } 252 253 // ListResources returns the list of resources based on the given request. 254 func (p *Provider) ListResources(req *pb.ListResourcesRequest) (*pb.ListResourcesResponse, error) { 255 fPath := req.GetResourcePath() 256 if fPath != "" { 257 ls := p.listers[fPath] 258 if ls == nil { 259 return nil, fmt.Errorf("file path %s is not available on this server", fPath) 260 } 261 return responseWithCacheCheck(ls, req) 262 } 263 264 // Avoid append and another allocation if there is only one lister, most 265 // common use case. 266 if len(p.listers) == 1 { 267 for _, ls := range p.listers { 268 return responseWithCacheCheck(ls, req) 269 } 270 } 271 272 // If we are working with multiple listers, it's slightly more complicated. 273 // In that case we need to return all the listers' resources even if only one 274 // of them has changed. 275 // 276 // Get the latest last-modified. 277 lastModified := int64(0) 278 for _, ls := range p.listers { 279 listerLastModified := ls.lastModified() 280 if lastModified < listerLastModified { 281 lastModified = listerLastModified 282 } 283 } 284 resp := &pb.ListResourcesResponse{ 285 LastModified: proto.Int64(lastModified), 286 } 287 288 // if nothing changed since req.IfModifiedSince, return early. 289 if req.GetIfModifiedSince() != 0 && lastModified <= req.GetIfModifiedSince() { 290 return resp, nil 291 } 292 293 var result []*pb.Resource 294 for _, fp := range p.filePaths { 295 res, err := p.listers[fp].listResources(req) 296 if err != nil { 297 return nil, err 298 } 299 result = append(result, res.Resources...) 300 } 301 resp.Resources = result 302 return resp, nil 303 } 304 305 // Provider provides a file-based targets provider for RDS. It implements the 306 // RDS server's Provider interface. 307 type Provider struct { 308 filePaths []string 309 listers map[string]*lister 310 } 311 312 // New creates a File (file) provider for RDS server, based on the 313 // provided config. 314 func New(c *configpb.ProviderConfig, l *logger.Logger) (*Provider, error) { 315 filePaths := c.GetFilePath() 316 p := &Provider{ 317 filePaths: filePaths, 318 listers: make(map[string]*lister), 319 } 320 321 for _, filePath := range filePaths { 322 lister, err := newLister(filePath, c, l) 323 if err != nil { 324 return nil, err 325 } 326 p.listers[filePath] = lister 327 } 328 329 return p, nil 330 }