github.com/kubeshop/testkube@v1.17.23/pkg/imageinspector/configmapstorage.go (about)

     1  package imageinspector
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"slices"
     7  	"sync"
     8  	"time"
     9  
    10  	"github.com/pkg/errors"
    11  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    12  
    13  	"github.com/kubeshop/testkube/pkg/configmap"
    14  )
    15  
    16  type configmapStorage struct {
    17  	client         configmap.Interface
    18  	name           string
    19  	avoidDirectGet bool // if there is memory storage prior to this one, all the contents will be copied there anyway
    20  	mu             sync.RWMutex
    21  }
    22  
    23  func NewConfigMapStorage(client configmap.Interface, name string, avoidDirectGet bool) StorageWithTransfer {
    24  	return &configmapStorage{
    25  		client:         client,
    26  		name:           name,
    27  		avoidDirectGet: avoidDirectGet,
    28  	}
    29  }
    30  
    31  func (c *configmapStorage) fetch(ctx context.Context) (map[string]string, error) {
    32  	c.mu.RLock()
    33  	defer c.mu.RUnlock()
    34  	cache, err := c.client.Get(ctx, c.name)
    35  	if err != nil && !k8serrors.IsNotFound(err) {
    36  		return nil, errors.Wrap(err, "getting configmap cache")
    37  	}
    38  	if cache == nil {
    39  		cache = map[string]string{}
    40  	}
    41  	return cache, nil
    42  }
    43  
    44  func cleanOldRecords(currentData *map[string]string) {
    45  	// Unmarshal the fetched date for the records
    46  	type Entry struct {
    47  		time time.Time
    48  		name string
    49  	}
    50  	dates := make([]Entry, 0, len(*currentData))
    51  	var vv Info
    52  	for k := range *currentData {
    53  		_ = json.Unmarshal([]byte((*currentData)[k]), &vv)
    54  		dates = append(dates, Entry{time: vv.FetchedAt, name: k})
    55  	}
    56  	slices.SortFunc(dates, func(a, b Entry) int {
    57  		if a.time.Before(b.time) {
    58  			return -1
    59  		}
    60  		return 1
    61  	})
    62  
    63  	// Delete half of the records
    64  	for i := 0; i < len(*currentData)/2; i++ {
    65  		delete(*currentData, dates[i].name)
    66  	}
    67  }
    68  
    69  func (c *configmapStorage) save(ctx context.Context, serializedData map[string]string) error {
    70  	c.mu.Lock()
    71  	defer c.mu.Unlock()
    72  
    73  	// Save data
    74  	err := c.client.Apply(ctx, c.name, serializedData)
    75  
    76  	// When the cache is too big, delete the oldest items and try again
    77  	if err != nil && k8serrors.IsRequestEntityTooLargeError(err) {
    78  		cleanOldRecords(&serializedData)
    79  		err = c.client.Apply(ctx, c.name, serializedData)
    80  	}
    81  	return err
    82  }
    83  
    84  func (c *configmapStorage) StoreMany(ctx context.Context, data map[Hash]Info) (err error) {
    85  	if data == nil {
    86  		return
    87  	}
    88  	serialized, err := c.fetch(ctx)
    89  	if err != nil {
    90  		return
    91  	}
    92  	for k, v := range data {
    93  		serialized[string(k)], err = marshalInfo(v)
    94  		if err != nil {
    95  			return
    96  		}
    97  	}
    98  	return c.save(ctx, serialized)
    99  }
   100  
   101  func (c *configmapStorage) CopyTo(ctx context.Context, other ...StorageTransfer) (err error) {
   102  	serialized, err := c.fetch(ctx)
   103  	if err != nil {
   104  		return
   105  	}
   106  	data := make(map[Hash]Info, len(serialized))
   107  	for k, v := range serialized {
   108  		data[Hash(k)], err = unmarshalInfo(v)
   109  		if err != nil {
   110  			return
   111  		}
   112  	}
   113  	for _, v := range other {
   114  		err = v.StoreMany(ctx, data)
   115  		if err != nil {
   116  			return
   117  		}
   118  	}
   119  	return
   120  }
   121  
   122  func (c *configmapStorage) Store(ctx context.Context, request RequestBase, info Info) error {
   123  	return c.StoreMany(ctx, map[Hash]Info{
   124  		hash(request.Registry, request.Image): info,
   125  	})
   126  }
   127  
   128  func (c *configmapStorage) Get(ctx context.Context, request RequestBase) (*Info, error) {
   129  	if c.avoidDirectGet {
   130  		return nil, nil
   131  	}
   132  	data, err := c.fetch(ctx)
   133  	if err != nil {
   134  		return nil, err
   135  	}
   136  	value, ok := data[string(hash(request.Registry, request.Image))]
   137  	if !ok {
   138  		return nil, nil
   139  	}
   140  	v, err := unmarshalInfo(value)
   141  	if err != nil {
   142  		return nil, err
   143  	}
   144  	return &v, nil
   145  }