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  }