go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cipd/appengine/impl/testutil/metadata.go (about)

     1  // Copyright 2018 The LUCI 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  package testutil
    16  
    17  import (
    18  	"context"
    19  	"sort"
    20  	"strings"
    21  	"sync"
    22  
    23  	"google.golang.org/protobuf/proto"
    24  
    25  	api "go.chromium.org/luci/cipd/api/cipd/v1"
    26  	"go.chromium.org/luci/cipd/appengine/impl/metadata"
    27  	"go.chromium.org/luci/cipd/common"
    28  )
    29  
    30  // MetadataStore implements metadata.Storage using memory, for tests.
    31  //
    32  // Not terribly efficient, shouldn't be used with a large number of entries.
    33  type MetadataStore struct {
    34  	l     sync.Mutex
    35  	metas map[string]*api.PrefixMetadata // e.g. "/a/b/c/" => metadata
    36  }
    37  
    38  // Populate adds a metadata entry to the storage.
    39  //
    40  // If populates Prefix and Fingerprint. Returns the added item. Panics if the
    41  // prefix is bad or the given metadata is empty.
    42  func (s *MetadataStore) Populate(prefix string, m *api.PrefixMetadata) *api.PrefixMetadata {
    43  	meta, err := s.UpdateMetadata(context.Background(), prefix, func(_ context.Context, e *api.PrefixMetadata) error {
    44  		proto.Reset(e)
    45  		proto.Merge(e, m)
    46  		return nil
    47  	})
    48  	if err != nil {
    49  		panic(err)
    50  	}
    51  	if meta == nil {
    52  		panic("Populate should be used only with non-empty metadata")
    53  	}
    54  	return meta
    55  }
    56  
    57  // Purge removes metadata entry for some prefix.
    58  //
    59  // Panics if the prefix is bad. Purging missing metadata is noop.
    60  func (s *MetadataStore) Purge(prefix string) {
    61  	prefix, err := normPrefix(prefix)
    62  	if err != nil {
    63  		panic(err)
    64  	}
    65  
    66  	s.l.Lock()
    67  	defer s.l.Unlock()
    68  	delete(s.metas, prefix)
    69  }
    70  
    71  // GetMetadata fetches metadata associated with the given prefix and all
    72  // parent prefixes.
    73  func (s *MetadataStore) GetMetadata(ctx context.Context, prefix string) ([]*api.PrefixMetadata, error) {
    74  	prefix, err := normPrefix(prefix)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	s.l.Lock()
    80  	defer s.l.Unlock()
    81  
    82  	var metas []*api.PrefixMetadata
    83  	for p := range s.metas {
    84  		if strings.HasPrefix(prefix, p) {
    85  			metas = append(metas, cloneMetadata(s.metas[p]))
    86  		}
    87  	}
    88  
    89  	sort.Slice(metas, func(i, j int) bool {
    90  		return metas[i].Prefix < metas[j].Prefix
    91  	})
    92  	return metas, nil
    93  }
    94  
    95  // VisitMetadata performs depth-first enumeration of the metadata graph.
    96  func (s *MetadataStore) VisitMetadata(ctx context.Context, prefix string, cb metadata.Visitor) error {
    97  	prefix, err := normPrefix(prefix)
    98  	if err != nil {
    99  		return err
   100  	}
   101  	return s.asGraph(prefix).traverse(func(n *node) (cont bool, err error) {
   102  		// If this node represents a path element without actual metadata attached
   103  		// to it, just recurse deeper until we find some metadata. The exception is
   104  		// the root prefix itself, since per VisitMetadata contract, we must visit
   105  		// it even if it has no metadata directly attached to it.
   106  		if !n.hasMeta && n.path != prefix {
   107  			return true, nil
   108  		}
   109  
   110  		// Convert the prefix back to the form expected by the public API.
   111  		clean := strings.Trim(n.path, "/")
   112  
   113  		// Grab full metadata for it (including inherited one).
   114  		md, err := s.GetMetadata(ctx, clean)
   115  		if err != nil {
   116  			return false, err
   117  		}
   118  
   119  		// And ask the callback whether we should proceed.
   120  		return cb(clean, md)
   121  	})
   122  }
   123  
   124  // UpdateMetadata transactionally updates or creates metadata of some
   125  // prefix.
   126  func (s *MetadataStore) UpdateMetadata(ctx context.Context, prefix string, cb func(ctx context.Context, m *api.PrefixMetadata) error) (*api.PrefixMetadata, error) {
   127  	prefix, err := common.ValidatePackagePrefix(prefix)
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  
   132  	s.l.Lock()
   133  	defer s.l.Unlock()
   134  
   135  	key, _ := normPrefix(prefix)
   136  	before := s.metas[key] // the metadata before the callback
   137  	if before == nil {
   138  		before = &api.PrefixMetadata{Prefix: prefix}
   139  	}
   140  
   141  	// Don't let the callback modify or retain the internal data.
   142  	meta := cloneMetadata(before)
   143  	if err := cb(ctx, meta); err != nil {
   144  		return nil, err
   145  	}
   146  
   147  	// Don't let the callback mess with the prefix or the fingerprint.
   148  	meta.Prefix = before.Prefix
   149  	meta.Fingerprint = before.Fingerprint
   150  
   151  	// No changes at all? Return nil if the metadata didn't exist and wasn't
   152  	// created by the callback. Otherwise return the existing metadata.
   153  	if proto.Equal(before, meta) {
   154  		if before.Fingerprint == "" {
   155  			return nil, nil
   156  		}
   157  		return meta, nil
   158  	}
   159  
   160  	// Calculate the new fingerprint and put the metadata into the storage.
   161  	metadata.CalculateFingerprint(meta)
   162  	if s.metas == nil {
   163  		s.metas = make(map[string]*api.PrefixMetadata, 1)
   164  	}
   165  	s.metas[key] = cloneMetadata(meta)
   166  	return meta, nil
   167  }
   168  
   169  ////////////////////////////////////////////////////////////////////////////////
   170  
   171  type node struct {
   172  	path     string           // full path from the metadata root, e.g. "/a/b/c/"
   173  	children map[string]*node // keys are elementary path components
   174  	hasMeta  bool             // true if this node has metadata attached to it
   175  }
   176  
   177  // child returns a child node, creating it if necessary.
   178  func (n *node) child(name string) *node {
   179  	if c, ok := n.children[name]; ok {
   180  		return c
   181  	}
   182  	if n.children == nil {
   183  		n.children = make(map[string]*node, 1)
   184  	}
   185  	c := &node{path: n.path + name + "/"}
   186  	n.children[name] = c
   187  	return c
   188  }
   189  
   190  // traverse does depth-first traversal of the node's subtree starting from self.
   191  //
   192  // Children are visited in lexicographical order.
   193  func (n *node) traverse(cb func(*node) (cont bool, err error)) error {
   194  	switch descend, err := cb(n); {
   195  	case err != nil:
   196  		return err
   197  	case !descend:
   198  		return nil
   199  	}
   200  
   201  	keys := make([]string, 0, len(n.children))
   202  	for k := range n.children {
   203  		keys = append(keys, k)
   204  	}
   205  	sort.Strings(keys)
   206  
   207  	for _, k := range keys {
   208  		if err := n.children[k].traverse(cb); err != nil {
   209  			return err
   210  		}
   211  	}
   212  	return nil
   213  }
   214  
   215  // asGraph builds a graph representation of metadata subtree at the given
   216  // prefix.
   217  //
   218  // The returned root node represents 'prefix'.
   219  func (s *MetadataStore) asGraph(prefix string) *node {
   220  	s.l.Lock()
   221  	defer s.l.Unlock()
   222  
   223  	root := &node{path: prefix}
   224  	for pfx := range s.metas {
   225  		if !strings.HasPrefix(pfx, prefix) {
   226  			continue
   227  		}
   228  		// Convert "/<prefix>/a/b/c/" to "a/b/c".
   229  		rel := strings.TrimRight(strings.TrimPrefix(pfx, prefix), "/")
   230  		cur := root
   231  		if rel != "" {
   232  			for _, elem := range strings.Split(rel, "/") {
   233  				cur = cur.child(elem)
   234  			}
   235  		}
   236  		cur.hasMeta = true
   237  	}
   238  	return root
   239  }
   240  
   241  // normPrefix takes "a/b/c" and returns "/a/b/c/".
   242  //
   243  // For "" returns "/".
   244  func normPrefix(p string) (string, error) {
   245  	p, err := common.ValidatePackagePrefix(p)
   246  	if err != nil {
   247  		return "", err
   248  	}
   249  	if p == "" {
   250  		return "/", nil
   251  	}
   252  	return "/" + p + "/", nil
   253  }
   254  
   255  // cloneMetadata makes a deep copy of 'm'.
   256  func cloneMetadata(m *api.PrefixMetadata) *api.PrefixMetadata {
   257  	return proto.Clone(m).(*api.PrefixMetadata)
   258  }