Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions central/resourcecollection/datastore/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ import (
"github.com/stackrox/rox/central/resourcecollection/datastore/store/postgres"
v1 "github.com/stackrox/rox/generated/api/v1"
"github.com/stackrox/rox/generated/storage"
searchPkg "github.com/stackrox/rox/pkg/search"
pkgSearch "github.com/stackrox/rox/pkg/search"
)

//go:generate mockgen-wrapper

// DataStore is the entry point for modifying Collection data.
type DataStore interface {
Search(ctx context.Context, q *v1.Query) ([]searchPkg.Result, error)
Search(ctx context.Context, q *v1.Query) ([]pkgSearch.Result, error)
SearchResults(ctx context.Context, q *v1.Query) ([]*v1.SearchResult, error)
SearchCollections(ctx context.Context, q *v1.Query) ([]*storage.ResourceCollection, error)

Expand All @@ -34,7 +34,33 @@ type DataStore interface {
DryRunAddCollection(ctx context.Context, collection *storage.ResourceCollection) error
UpdateCollection(ctx context.Context, collection *storage.ResourceCollection) error
DryRunUpdateCollection(ctx context.Context, collection *storage.ResourceCollection) error
ResolveListDeployments(ctx context.Context, collection *storage.ResourceCollection) ([]*storage.ListDeployment, error)
// autocomplete workflow, maybe SearchResults? TODO ROX-12616

// ResolveCollectionQuery exported exclusively for testing purposes, should be hidden once e2e tests go in
// ResolveCollectionQuery(ctx context.Context, collection *storage.ResourceCollection) (*v1.Query, error)
}

var (
supportedFieldNames = map[string]pkgSearch.FieldLabel{
pkgSearch.Cluster.String(): pkgSearch.Cluster,
pkgSearch.ClusterLabel.String(): pkgSearch.ClusterLabel,
pkgSearch.Namespace.String(): pkgSearch.Namespace,
pkgSearch.NamespaceLabel.String(): pkgSearch.NamespaceLabel,
pkgSearch.NamespaceAnnotation.String(): pkgSearch.NamespaceAnnotation,
pkgSearch.DeploymentName.String(): pkgSearch.DeploymentName,
pkgSearch.DeploymentLabel.String(): pkgSearch.DeploymentLabel,
pkgSearch.DeploymentAnnotation.String(): pkgSearch.DeploymentAnnotation,
}
)

// GetSupportedFieldLabels returns a list of the supported search.FieldLabel values for resolving deployments for a collection
func GetSupportedFieldLabels() []pkgSearch.FieldLabel {
var ret []pkgSearch.FieldLabel
for _, label := range supportedFieldNames {
ret = append(ret, label)
}
return ret
}

// New returns a new instance of a DataStore.
Expand Down
166 changes: 163 additions & 3 deletions central/resourcecollection/datastore/datastore_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package datastore

import (
"context"
"regexp"

"github.com/heimdalr/dag"
"github.com/pkg/errors"
deploymentDS "github.com/stackrox/rox/central/deployment/datastore"
"github.com/stackrox/rox/central/resourcecollection/datastore/index"
"github.com/stackrox/rox/central/resourcecollection/datastore/search"
"github.com/stackrox/rox/central/resourcecollection/datastore/store"
Expand Down Expand Up @@ -34,19 +36,24 @@ type datastoreImpl struct {
indexer index.Indexer
searcher search.Searcher

deploymentDS deploymentDS.DataStore

lock sync.RWMutex
graph *dag.DAG
names set.Set[string]
}

// graphEntry is the stored object in the dag.DAG
type graphEntry struct {
id string
}

// ID returns the id of the object
func (ge graphEntry) ID() string {
return ge.id
}

// initGraph initializes the dag.DAG for a given datastore instance, should be invoked during init time
func (ds *datastoreImpl) initGraph() error {

// build graph object
Expand Down Expand Up @@ -262,7 +269,7 @@ func (ds *datastoreImpl) GetMany(ctx context.Context, ids []string) ([]*storage.
func (ds *datastoreImpl) addCollectionWorkflow(ctx context.Context, collection *storage.ResourceCollection, dryrun bool) error {

// sanity checks
if err := verifyCollectionObjectNotEmpty(collection); err != nil {
if err := verifyCollectionConstraints(collection); err != nil {
return err
}
if collection.GetId() != "" {
Expand Down Expand Up @@ -317,7 +324,7 @@ func (ds *datastoreImpl) DryRunAddCollection(ctx context.Context, collection *st
func (ds *datastoreImpl) updateCollectionWorkflow(ctx context.Context, collection *storage.ResourceCollection, dryrun bool) error {

// sanity checks
if err := verifyCollectionObjectNotEmpty(collection); err != nil {
if err := verifyCollectionConstraints(collection); err != nil {
return err
}
if collection.GetId() == "" {
Expand All @@ -329,7 +336,7 @@ func (ds *datastoreImpl) updateCollectionWorkflow(ctx context.Context, collectio
return err
}

// if this a dryrun we don't ever end up calling upsert so we only need to get a read lock
// if this a dryrun we don't ever end up calling upsert, so we only need to get a read lock
if dryrun {
ds.lock.RLock()
defer ds.lock.RUnlock()
Expand Down Expand Up @@ -418,3 +425,156 @@ func verifyCollectionObjectNotEmpty(obj *storage.ResourceCollection) error {
}
return nil
}

func (ds *datastoreImpl) ResolveListDeployments(ctx context.Context, collection *storage.ResourceCollection) ([]*storage.ListDeployment, error) {

if err := verifyCollectionConstraints(collection); err != nil {
return nil, err
}

query, err := ds.resolveCollectionQuery(ctx, collection)
if err != nil {
return nil, err
}
return ds.deploymentDS.SearchListDeployments(ctx, query)
}

func (ds *datastoreImpl) resolveCollectionQuery(ctx context.Context, collection *storage.ResourceCollection) (*v1.Query, error) {
var collections []*storage.ResourceCollection
var collectionSet set.Set[string]
var disjunctions []*v1.Query

collections = append(collections, collection)

ds.lock.RLock()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why lock?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we have to expand the collection objects that are embedded by querying the datastore level for those objects, so we take a read lock out to ensure that the objects don't go stale or get removed halfway through the expansion. For the future I envision the main thread being a process of just resolving these objects recursively until we've resolved the whole tree of objects, and spawn go routines as we go to turn the objects into queries

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

defer ds.lock.RUnlock()

for len(collections) > 0 {

// get first index and remove from list
collection := collections[0]
collections = collections[1:]

if !collectionSet.Add(collection.GetId()) {
continue
}

// resolve the collection to queries
queries, err := collectionToQueries(collection)
if err != nil {
return nil, err
}
disjunctions = append(disjunctions, queries...)

// add embedded values
embeddedList, _, err := ds.storage.GetMany(ctx, embeddedCollectionsToIDList(collection.GetEmbeddedCollections()))
if err != nil {
return nil, err
}
collections = append(collections, embeddedList...)
}

return pkgSearch.DisjunctionQuery(disjunctions...), nil
}

// collectionToQueries returns a list of queries derived from the given resource collection's storage.ResourceSelector list
// these should be combined as disjunct with any resolved embedded queries
func collectionToQueries(collection *storage.ResourceCollection) ([]*v1.Query, error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

put it under resourcecollection/utils/

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as below

var ret []*v1.Query

for _, resourceSelector := range collection.GetResourceSelectors() {
var selectorRuleQueries []*v1.Query
for _, selectorRule := range resourceSelector.GetRules() {

fieldLabel, present := supportedFieldNames[selectorRule.GetFieldName()]
if !present {
return nil, errors.Wrapf(errox.InvalidArgs, "unsupported field name %q", selectorRule.GetFieldName())
}

ruleValueQueries := ruleValuesToQueryList(fieldLabel, selectorRule.GetValues())
if len(ruleValueQueries) > 0 {
// rule values are conjunct or disjunct with each other depending on the operator
switch selectorRule.GetOperator() {
case storage.BooleanOperator_OR:
selectorRuleQueries = append(selectorRuleQueries, pkgSearch.DisjunctionQuery(ruleValueQueries...))
case storage.BooleanOperator_AND:
selectorRuleQueries = append(selectorRuleQueries, pkgSearch.ConjunctionQuery(ruleValueQueries...))
default:
return nil, errors.Wrap(errox.InvalidArgs, "unsupported boolean operator")
}
}
}
if len(selectorRuleQueries) > 0 {
// selector rules are conjunct with each other
ret = append(ret, pkgSearch.ConjunctionQuery(selectorRuleQueries...))
}
}

return ret, nil
}

func ruleValuesToQueryList(fieldLabel pkgSearch.FieldLabel, ruleValues []*storage.RuleValue) []*v1.Query {
ret := make([]*v1.Query, 0, len(ruleValues))
for _, ruleValue := range ruleValues {
ret = append(ret, pkgSearch.NewQueryBuilder().AddRegexes(fieldLabel, ruleValue.GetValue()).ProtoQuery())
}
return ret
}

func embeddedCollectionsToIDList(embeddedList []*storage.ResourceCollection_EmbeddedResourceCollection) []string {
ret := make([]string, 0, len(embeddedList))
for _, embedded := range embeddedList {
ret = append(ret, embedded.GetId())
}
return ret
}

// verifyCollectionConstraints ensures the given collection is valid according to implementation constraints
// - the collection object is not nil
// - there is at most one storage.ResourceSelector
// - only storage.BooleanOperator_OR is supported as an operator
// - all storage.SelectorRule "FieldName" values are valid
// - all storage.RuleValue fields compile as valid regex
// - storage.RuleValue fields supplied when "FieldName" values provided
func verifyCollectionConstraints(collection *storage.ResourceCollection) error {

// object not nil
if collection == nil {
return errors.New("passed collection must be non nil")
}

// currently we only support one resource selector per collection from UX
if collection.GetResourceSelectors() != nil && len(collection.GetResourceSelectors()) > 1 {
return errors.Wrap(errox.InvalidArgs, "only 1 resource selector is supported per collection")
}

for _, resourceSelector := range collection.GetResourceSelectors() {
for _, selectorRule := range resourceSelector.GetRules() {

// currently we only support disjunction (OR) operations
if selectorRule.GetOperator() != storage.BooleanOperator_OR {
return errors.Wrapf(errox.InvalidArgs, "%q boolean operator unsupported", selectorRule.GetOperator().String())
}

// we have a short list of supported field name values
_, present := supportedFieldNames[selectorRule.GetFieldName()]
if !present {
return errors.Wrapf(errox.InvalidArgs, "unsupported field name %q", selectorRule.GetFieldName())
}

// we require at least one value if a field name is set
if len(selectorRule.GetValues()) == 0 {
return errors.Wrap(errox.InvalidArgs, "rule values required with a set field name")
}
for _, ruleValue := range selectorRule.GetValues() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A rule with field name but no value is malformed. Add a check.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a check for this in the verify collection function. Also refactored some of the tests out into a test for just that function


// rule values must be valid regex
if _, err := regexp.Compile(ruleValue.GetValue()); err != nil {
return errors.Wrap(errors.Wrap(err, errox.InvalidArgs.Error()), "failed to compile rule value regex")
}
}
}
}

return nil
}
Loading