diff --git a/go.mod b/go.mod index a09aaf3123..9197dc21b4 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/goccy/go-json v0.10.3 github.com/golang-jwt/jwt/v4 v4.5.0 github.com/golang-migrate/migrate/v4 v4.17.1 + github.com/google/cel-go v0.20.1 github.com/google/go-cmp v0.6.0 github.com/google/go-containerregistry v0.20.1 github.com/google/go-github/v63 v63.0.0 @@ -119,7 +120,6 @@ require ( github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect - github.com/google/cel-go v0.20.1 // indirect github.com/google/go-github/v61 v61.0.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/hashicorp/go-sockaddr v1.0.5 // indirect diff --git a/internal/engine/selectors/mock/selectors.go b/internal/engine/selectors/mock/selectors.go new file mode 100644 index 0000000000..65bcf293e3 --- /dev/null +++ b/internal/engine/selectors/mock/selectors.go @@ -0,0 +1,100 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./selectors.go +// +// Generated by this command: +// +// mockgen -package mock_selectors -destination=./mock/selectors.go -source=./selectors.go +// + +// Package mock_selectors is a generated GoMock package. +package mock_selectors + +import ( + reflect "reflect" + + selectors "github.com/stacklok/minder/internal/engine/selectors" + proto "github.com/stacklok/minder/internal/proto" + v1 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1" + gomock "go.uber.org/mock/gomock" +) + +// MockSelectionBuilder is a mock of SelectionBuilder interface. +type MockSelectionBuilder struct { + ctrl *gomock.Controller + recorder *MockSelectionBuilderMockRecorder +} + +// MockSelectionBuilderMockRecorder is the mock recorder for MockSelectionBuilder. +type MockSelectionBuilderMockRecorder struct { + mock *MockSelectionBuilder +} + +// NewMockSelectionBuilder creates a new mock instance. +func NewMockSelectionBuilder(ctrl *gomock.Controller) *MockSelectionBuilder { + mock := &MockSelectionBuilder{ctrl: ctrl} + mock.recorder = &MockSelectionBuilderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSelectionBuilder) EXPECT() *MockSelectionBuilderMockRecorder { + return m.recorder +} + +// NewSelectionFromProfile mocks base method. +func (m *MockSelectionBuilder) NewSelectionFromProfile(arg0 v1.Entity, arg1 []*v1.Profile_Selector) (selectors.Selection, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewSelectionFromProfile", arg0, arg1) + ret0, _ := ret[0].(selectors.Selection) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NewSelectionFromProfile indicates an expected call of NewSelectionFromProfile. +func (mr *MockSelectionBuilderMockRecorder) NewSelectionFromProfile(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewSelectionFromProfile", reflect.TypeOf((*MockSelectionBuilder)(nil).NewSelectionFromProfile), arg0, arg1) +} + +// MockSelection is a mock of Selection interface. +type MockSelection struct { + ctrl *gomock.Controller + recorder *MockSelectionMockRecorder +} + +// MockSelectionMockRecorder is the mock recorder for MockSelection. +type MockSelectionMockRecorder struct { + mock *MockSelection +} + +// NewMockSelection creates a new mock instance. +func NewMockSelection(ctrl *gomock.Controller) *MockSelection { + mock := &MockSelection{ctrl: ctrl} + mock.recorder = &MockSelectionMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSelection) EXPECT() *MockSelectionMockRecorder { + return m.recorder +} + +// Select mocks base method. +func (m *MockSelection) Select(arg0 *proto.SelectorEntity, arg1 ...selectors.SelectOption) (bool, error) { + m.ctrl.T.Helper() + varargs := []any{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Select", varargs...) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Select indicates an expected call of Select. +func (mr *MockSelectionMockRecorder) Select(arg0 any, arg1 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*MockSelection)(nil).Select), varargs...) +} diff --git a/internal/engine/selectors/selectors.go b/internal/engine/selectors/selectors.go new file mode 100644 index 0000000000..966c24c4a3 --- /dev/null +++ b/internal/engine/selectors/selectors.go @@ -0,0 +1,375 @@ +// +// Copyright 2024 Stacklok, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:generate go run go.uber.org/mock/mockgen -package mock_$GOPACKAGE -destination=./mock/$GOFILE -source=./$GOFILE + +// Package selectors provides utilities for selecting entities based on profiles using CEL +package selectors + +import ( + "errors" + "fmt" + "strings" + "sync" + + "github.com/google/cel-go/cel" + "github.com/google/cel-go/checker/decls" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/interpreter" + + internalpb "github.com/stacklok/minder/internal/proto" + minderv1 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1" +) + +var ( + // ErrResultUnknown is returned when the result of a selector expression is unknown + // this tells the caller to try again with more information + ErrResultUnknown = errors.New("result is unknown") +) + +// celEnvFactory is an interface for creating CEL environments +// for an entity. Each entity must implement this interface to be +// usable in selectors +type celEnvFactory func() (*cel.Env, error) + +// genericEnvFactory is a factory for creating a CEL environment +// for the generic SelectorEntity type +func genericEnvFactory() (*cel.Env, error) { + return newEnvForEntity( + "entity", + &internalpb.SelectorEntity{}, + "internal.SelectorEntity") +} + +// repoEnvFactory is a factory for creating a CEL environment +// for the SelectorRepository type representing a repository +func repoEnvFactory() (*cel.Env, error) { + return newEnvForEntity( + "repository", + &internalpb.SelectorRepository{}, + "internal.SelectorRepository") +} + +// artifactEnvFactory is a factory for creating a CEL environment +// for the SelectorArtifact type representing an artifact +func artifactEnvFactory() (*cel.Env, error) { + return newEnvForEntity( + "artifact", + &internalpb.SelectorArtifact{}, + "internal.SelectorArtifact") +} + +// newEnvForEntity creates a new CEL environment for an entity. All environments are allowed to +// use the generic "entity" variable plus the specific entity type is also declared as variable +// with the appropriate type. +func newEnvForEntity(varName string, typ any, typName string) (*cel.Env, error) { + entityPtr := &internalpb.SelectorEntity{} + + env, err := cel.NewEnv( + cel.Types(typ), cel.Types(&internalpb.SelectorEntity{}), + cel.Declarations( + decls.NewVar("entity", + decls.NewObjectType(string(entityPtr.ProtoReflect().Descriptor().FullName())), + ), + decls.NewVar(varName, + decls.NewObjectType(typName), + ), + ), + ) + if err != nil { + return nil, fmt.Errorf("failed to create CEL environment for %s: %v", varName, err) + } + + return env, nil +} + +type compiledSelector struct { + ast *cel.Ast + program cel.Program +} + +// compileSelectorForEntity compiles a selector expression for a given entity type into a CEL program +func compileSelectorForEntity(env *cel.Env, selector string) (*compiledSelector, error) { + ast, issues := env.Parse(selector) + if issues.Err() != nil { + return nil, fmt.Errorf("failed to parse expression %q: %w", selector, issues.Err()) + } + + checked, issues := env.Check(ast) + if issues.Err() != nil { + return nil, fmt.Errorf("failed to check expression %q: %w", selector, issues.Err()) + } + + program, err := env.Program(checked, + // OptPartialEval is needed to enable partial evaluation of the expression + // OptTrackState is needed to get the details about partial evaluation (aka what is missing) + cel.EvalOptions(cel.OptTrackState, cel.OptPartialEval)) + if err != nil { + return nil, fmt.Errorf("failed to create program for expression %q: %w", selector, err) + } + + return &compiledSelector{ + ast: checked, + program: program, + }, nil +} + +// SelectionBuilder is an interface for creating Selections (a collection of compiled CEL expressions) +// for an entity type. This is what the user of this module uses. The interface makes it easier to pass +// mocks by the user of this module. +type SelectionBuilder interface { + NewSelectionFromProfile(minderv1.Entity, []*minderv1.Profile_Selector) (Selection, error) +} + +// Env is a struct that holds the CEL environments for each entity type and the factories for creating +type Env struct { + // entityEnvs is a map of entity types to their respective CEL environments. We keep them cached + // and lazy-initialize on first use + entityEnvs map[minderv1.Entity]*entityEnvCache + // factories is a map of entity types to their respective factories for creating CEL environments + factories map[minderv1.Entity]celEnvFactory +} + +// entityEnvCache is a struct that holds a CEL environment for lazy-initialization. Since the initialization +// is done only once, we also keep track of the error +type entityEnvCache struct { + once sync.Once + env *cel.Env + err error +} + +// NewEnv creates a new Env struct with the default factories for each entity type. The factories +// are used on first access to create the CEL environments for each entity type. +func NewEnv() (*Env, error) { + factoryMap := map[minderv1.Entity]celEnvFactory{ + minderv1.Entity_ENTITY_UNSPECIFIED: genericEnvFactory, + minderv1.Entity_ENTITY_REPOSITORIES: repoEnvFactory, + minderv1.Entity_ENTITY_ARTIFACTS: artifactEnvFactory, + // TODO(jakub): Add pull requests when we add then to the selector protobuf + } + + entityEnvs := make(map[minderv1.Entity]*entityEnvCache, len(factoryMap)) + for entity := range factoryMap { + entityEnvs[entity] = &entityEnvCache{} + } + + return &Env{ + entityEnvs: entityEnvs, + factories: factoryMap, + }, nil +} + +// NewSelectionFromProfile creates a new Selection (compiled CEL programs for that entity type) +// from a profile +func (e *Env) NewSelectionFromProfile( + entityType minderv1.Entity, + profileSelection []*minderv1.Profile_Selector, +) (Selection, error) { + selector := make([]*compiledSelector, 0, len(profileSelection)) + + env, err := e.envForEntity(entityType) + if err != nil { + return nil, fmt.Errorf("failed to get environment for entity %v: %w", entityType, err) + } + + for _, sel := range profileSelection { + ent := minderv1.EntityFromString(sel.GetEntity()) + if ent != entityType && ent != minderv1.Entity_ENTITY_UNSPECIFIED { + continue + } + + compSel, err := compileSelectorForEntity(env, sel.Selector) + if err != nil { + return nil, fmt.Errorf("failed to compile selector %q: %w", sel.Selector, err) + } + + selector = append(selector, compSel) + } + + return &EntitySelection{ + env: env, + selector: selector, + entity: entityType, + }, nil +} + +// envForEntity gets the CEL environment for a given entity type. If the environment is not cached, +// it creates it using the factory for that entity type. +func (e *Env) envForEntity(entity minderv1.Entity) (*cel.Env, error) { + cache, ok := e.entityEnvs[entity] + if !ok { + return nil, fmt.Errorf("no cache found for entity %v", entity) + } + + cache.once.Do(func() { + cache.env, cache.err = e.factories[entity]() + }) + + return cache.env, cache.err +} + +// SelectOption is a functional option for the Select method +type SelectOption func(*selectionOptions) + +type selectionOptions struct { + unknownPaths []string +} + +// WithUnknownPaths sets the explicit unknown paths for the selection +func WithUnknownPaths(paths ...string) SelectOption { + return func(o *selectionOptions) { + o.unknownPaths = paths + } +} + +// Selection is an interface for selecting entities based on a profile +type Selection interface { + Select(*internalpb.SelectorEntity, ...SelectOption) (bool, error) +} + +// EntitySelection is a struct that holds the compiled CEL expressions for a given entity type +type EntitySelection struct { + env *cel.Env + + selector []*compiledSelector + entity minderv1.Entity +} + +// Select return true if the entity matches all the compiled expressions and false otherwise +func (s *EntitySelection) Select(se *internalpb.SelectorEntity, userOpts ...SelectOption) (bool, error) { + if se == nil { + return false, fmt.Errorf("input entity is nil") + } + + var opts selectionOptions + for _, opt := range userOpts { + opt(&opts) + } + + for _, sel := range s.selector { + entityMap, err := inputAsMap(se) + if err != nil { + return false, fmt.Errorf("failed to convert input to map: %w", err) + } + + out, details, err := s.evalWithOpts(&opts, sel, entityMap) + // check unknowns /before/ an error. Maybe we should try to special-case the one + // error we get from the CEL library in this case and check for the rest? + if s.detailHasUnknowns(sel, details) { + return false, ErrResultUnknown + } + + if err != nil { + return false, fmt.Errorf("failed to evaluate Expression: %w", err) + } + + if types.IsUnknown(out) { + return false, ErrResultUnknown + } + + if out.Type() != cel.BoolType { + return false, fmt.Errorf("expression did not evaluate to a boolean: %v", out) + } + + if !out.Value().(bool) { + return false, nil + } + } + + return true, nil +} + +func unknownAttributesFromOpts(unknownPaths []string) []*interpreter.AttributePattern { + unknowns := make([]*interpreter.AttributePattern, 0, len(unknownPaths)) + + for _, path := range unknownPaths { + frags := strings.Split(path, ".") + if len(frags) == 0 { + continue + } + + unknownAttr := interpreter.NewAttributePattern(frags[0]) + if len(frags) > 1 { + for _, frag := range frags[1:] { + unknownAttr = unknownAttr.QualString(frag) + } + } + unknowns = append(unknowns, unknownAttr) + } + + return unknowns +} + +func (_ *EntitySelection) evalWithOpts( + opts *selectionOptions, sel *compiledSelector, entityMap map[string]any, +) (ref.Val, *cel.EvalDetails, error) { + unknowns := unknownAttributesFromOpts(opts.unknownPaths) + if len(unknowns) > 0 { + partialMap, err := cel.PartialVars(entityMap, unknowns...) + if err != nil { + return types.NewErr("failed to create partial value"), nil, fmt.Errorf("failed to create partial vars: %w", err) + } + + return sel.program.Eval(partialMap) + } + + return sel.program.Eval(entityMap) +} + +func (s *EntitySelection) detailHasUnknowns(sel *compiledSelector, details *cel.EvalDetails) bool { + if details == nil { + return false + } + + // TODO(jakub): We should also extract what the unknowns are and return them + // there exists cel.AstToString() which prints the part that was not evaluated, but as a whole + // (e.g. properties['is_fork'] == true) and not as a list of unknowns. We should either take a look + // at its implementation or walk the AST ourselves + residualAst, err := s.env.ResidualAst(sel.ast, details) + if err != nil { + return false + } + + checked, err := cel.AstToCheckedExpr(residualAst) + if err != nil { + return false + } + + return checked.GetExpr().GetConstExpr() == nil +} + +func inputAsMap(se *internalpb.SelectorEntity) (map[string]any, error) { + var value any + + key := se.GetEntityType().ToString() + + // FIXME(jakub): I tried to be smart and code something up using protoreflect and WhichOneOf but didn't + // make it work. Maybe someone smarter than me can. + // nolint:exhaustive + switch se.GetEntityType() { + case minderv1.Entity_ENTITY_REPOSITORIES: + value = se.GetRepository() + case minderv1.Entity_ENTITY_ARTIFACTS: + value = se.GetArtifact() + default: + return nil, fmt.Errorf("unsupported entity type [%d]: %s", se.GetEntityType(), se.GetEntityType().ToString()) + } + + return map[string]any{ + key: value, + "entity": se, + }, nil +} diff --git a/internal/engine/selectors/selectors_test.go b/internal/engine/selectors/selectors_test.go new file mode 100644 index 0000000000..e59c1d717d --- /dev/null +++ b/internal/engine/selectors/selectors_test.go @@ -0,0 +1,533 @@ +// +// Copyright 2024 Stacklok, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package selectors + +import ( + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" + + internalpb "github.com/stacklok/minder/internal/proto" + minderv1 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1" +) + +func TestNewSelectorEngine(t *testing.T) { + t.Parallel() + + env, err := NewEnv() + require.NoError(t, err) + require.NotNil(t, env) + require.NotNil(t, env.entityEnvs) + require.NotNil(t, env.entityEnvs[minderv1.Entity_ENTITY_REPOSITORIES]) + require.NotNil(t, env.entityEnvs[minderv1.Entity_ENTITY_ARTIFACTS]) +} + +type testSelectorEntityBuilder func() *internalpb.SelectorEntity +type testRepoOption func(selRepo *internalpb.SelectorRepository) +type testArtifactOption func(selArtifact *internalpb.SelectorArtifact) + +func newTestArtifactSelectorEntity(artifactOpts ...testArtifactOption) testSelectorEntityBuilder { + return func() *internalpb.SelectorEntity { + artifact := &internalpb.SelectorEntity{ + EntityType: minderv1.Entity_ENTITY_ARTIFACTS, + Name: "testorg/testartifact", + Entity: &internalpb.SelectorEntity_Artifact{ + Artifact: &internalpb.SelectorArtifact{ + Name: "testorg/testartifact", + Type: "container", + }, + }, + } + + for _, opt := range artifactOpts { + opt(artifact.Entity.(*internalpb.SelectorEntity_Artifact).Artifact) + } + + return artifact + } +} + +func newTestRepoSelectorEntity(repoOpts ...testRepoOption) testSelectorEntityBuilder { + return func() *internalpb.SelectorEntity { + repo := &internalpb.SelectorEntity{ + EntityType: minderv1.Entity_ENTITY_REPOSITORIES, + Name: "testorg/testrepo", + Entity: &internalpb.SelectorEntity_Repository{ + Repository: &internalpb.SelectorRepository{ + Name: "testorg/testrepo", + }, + }, + } + + for _, opt := range repoOpts { + opt(repo.Entity.(*internalpb.SelectorEntity_Repository).Repository) + } + + return repo + } +} + +func withIsFork(isFork bool) testRepoOption { + return func(selRepo *internalpb.SelectorRepository) { + selRepo.IsFork = &isFork + } +} + +func withProperties(properties map[string]any) testRepoOption { + return func(selRepo *internalpb.SelectorRepository) { + protoProperties, err := structpb.NewStruct(properties) + if err != nil { + panic(err) + } + selRepo.Properties = protoProperties + } +} + +func TestSelectSelectorEntity(t *testing.T) { + t.Parallel() + + scenarios := []struct { + name string + exprs []*minderv1.Profile_Selector + selectOptions []SelectOption + selectorEntityBld testSelectorEntityBuilder + expectedNewSelectionErr string + expectedSelectErr error + selected bool + }{ + { + name: "No selectors", + exprs: []*minderv1.Profile_Selector{}, + selectorEntityBld: newTestRepoSelectorEntity(), + selected: true, + }, + { + name: "Simple true repository expression", + exprs: []*minderv1.Profile_Selector{ + { + Entity: minderv1.RepositoryEntity.String(), + Selector: "repository.name == 'testorg/testrepo'", + }, + }, + selectorEntityBld: newTestRepoSelectorEntity(), + selected: true, + }, + { + name: "Simple true artifact expression", + exprs: []*minderv1.Profile_Selector{ + { + Entity: minderv1.ArtifactEntity.String(), + Selector: "artifact.type == 'container'", + }, + }, + selectorEntityBld: newTestArtifactSelectorEntity(), + selected: true, + }, + { + name: "Simple false artifact expression", + exprs: []*minderv1.Profile_Selector{ + { + Entity: minderv1.ArtifactEntity.String(), + Selector: "artifact.type != 'container'", + }, + }, + selectorEntityBld: newTestArtifactSelectorEntity(), + selected: false, + }, + { + name: "Simple false repository expression", + exprs: []*minderv1.Profile_Selector{ + { + Entity: minderv1.RepositoryEntity.String(), + Selector: "repository.name != 'testorg/testrepo'", + }, + }, + selectorEntityBld: newTestRepoSelectorEntity(), + selected: false, + }, + { + name: "Simple true generic entity expression for repo entity type", + exprs: []*minderv1.Profile_Selector{ + { + Entity: minderv1.RepositoryEntity.String(), + Selector: "entity.name == 'testorg/testrepo'", + }, + }, + selectorEntityBld: newTestRepoSelectorEntity(), + selected: true, + }, + { + name: "Simple false generic entity expression for repo entity type", + exprs: []*minderv1.Profile_Selector{ + { + Entity: minderv1.RepositoryEntity.String(), + Selector: "entity.name != 'testorg/testrepo'", + }, + }, + selectorEntityBld: newTestRepoSelectorEntity(), + selected: false, + }, + { + name: "Simple true generic entity expression for unspecified entity type", + exprs: []*minderv1.Profile_Selector{ + { + Entity: "", + Selector: "entity.name == 'testorg/testrepo'", + }, + }, + selectorEntityBld: newTestRepoSelectorEntity(), + selected: true, + }, + { + name: "Simple false generic entity expression for unspecified entity type", + exprs: []*minderv1.Profile_Selector{ + { + Entity: "", + Selector: "entity.name != 'testorg/testrepo'", + }, + }, + selectorEntityBld: newTestRepoSelectorEntity(), + selected: false, + }, + { + name: "Expressions for different types than the entity are skipped", + exprs: []*minderv1.Profile_Selector{ + { + Entity: minderv1.ArtifactEntity.String(), + Selector: "artifact.name != 'namespace/containername'", + }, + { + Entity: minderv1.PullRequestEntity.String(), + Selector: "pull_request.name != 'namespace/containername'", + }, + }, + selectorEntityBld: newTestRepoSelectorEntity(), + selected: true, + }, + { + name: "Expression on is_fork bool attribute set to true", + exprs: []*minderv1.Profile_Selector{ + { + Entity: minderv1.RepositoryEntity.String(), + Selector: "repository.is_fork == true", + }, + }, + selectorEntityBld: newTestRepoSelectorEntity(withIsFork(true)), + selected: true, + }, + { + name: "Expression on is_fork bool attribute set to false", + exprs: []*minderv1.Profile_Selector{ + { + Entity: minderv1.RepositoryEntity.String(), + Selector: "repository.is_fork == true", + }, + }, + selectorEntityBld: newTestRepoSelectorEntity(withIsFork(false)), + selected: false, + }, + { + name: "Expression on is_fork bool attribute set to nil and true expression", + exprs: []*minderv1.Profile_Selector{ + { + Entity: minderv1.RepositoryEntity.String(), + Selector: "repository.is_fork == true", + }, + }, + selectorEntityBld: newTestRepoSelectorEntity(), + selected: false, + }, + { + name: "Expression on is_fork bool attribute set to nil and false expression", + exprs: []*minderv1.Profile_Selector{ + { + Entity: minderv1.RepositoryEntity.String(), + Selector: "repository.is_fork == false", + }, + }, + selectorEntityBld: newTestRepoSelectorEntity(), + selected: true, + }, + { + name: "Wrong entity type - repo selector uses artifact", + exprs: []*minderv1.Profile_Selector{ + { + Entity: minderv1.RepositoryEntity.String(), + Selector: "artifact.name != 'testorg/testrepo'", + }, + }, + selectorEntityBld: newTestRepoSelectorEntity(), + expectedNewSelectionErr: "undeclared reference to 'artifact'", + selected: false, + }, + { + name: "Attempt to use a repo attribute that doesn't exist", + exprs: []*minderv1.Profile_Selector{ + { + Entity: minderv1.RepositoryEntity.String(), + Selector: "repository.iamnothere == 'value'", + }, + }, + selectorEntityBld: newTestRepoSelectorEntity(), + expectedNewSelectionErr: "undefined field 'iamnothere'", + selected: false, + }, + { + name: "Use a property that is defined and true result", + exprs: []*minderv1.Profile_Selector{ + { + Entity: minderv1.RepositoryEntity.String(), + Selector: "repository.properties.github['is_fork'] == false", + }, + }, + selectorEntityBld: newTestRepoSelectorEntity( + withProperties(map[string]any{ + "github": map[string]any{"is_fork": false}, + }), + ), + selected: true, + }, + { + name: "Use a string property that is defined and true result", + exprs: []*minderv1.Profile_Selector{ + { + Entity: minderv1.RepositoryEntity.String(), + Selector: "repository.properties.license == 'MIT'", + }, + }, + selectorEntityBld: newTestRepoSelectorEntity( + withProperties(map[string]any{ + "license": "MIT", + }), + ), + selected: true, + }, + { + name: "Use a string property that is defined and false result", + exprs: []*minderv1.Profile_Selector{ + { + Entity: minderv1.RepositoryEntity.String(), + Selector: "repository.properties.license == 'MIT'", + }, + }, + selectorEntityBld: newTestRepoSelectorEntity( + withProperties(map[string]any{ + "license": "BSD", + }), + ), + selected: false, + }, + { + name: "Use a property that is defined and false result", + exprs: []*minderv1.Profile_Selector{ + { + Entity: minderv1.RepositoryEntity.String(), + Selector: "repository.properties.github['is_fork'] == false", + }, + }, + selectorEntityBld: newTestRepoSelectorEntity( + withProperties(map[string]any{ + "github": map[string]any{"is_fork": true}, + }), + ), + selected: false, + }, + { + name: "Properties are non-nil but we use one that is not defined", + exprs: []*minderv1.Profile_Selector{ + { + Entity: minderv1.RepositoryEntity.String(), + Selector: "repository.properties.github['is_private'] != true", + }, + }, + selectorEntityBld: newTestRepoSelectorEntity( + withProperties(map[string]any{ + "github": map[string]any{"is_fork": true}, + }), + ), + expectedSelectErr: ErrResultUnknown, + selected: false, + }, + { + name: "Attempt to use a property while having nil properties", + exprs: []*minderv1.Profile_Selector{ + { + Entity: minderv1.RepositoryEntity.String(), + Selector: "repository.properties.github['is_fork'] != 'true'", + }, + }, + selectorEntityBld: newTestRepoSelectorEntity(), + expectedSelectErr: ErrResultUnknown, + selected: false, + }, + { + name: "The selector shortcuts if evaluation is not needed for properties", + exprs: []*minderv1.Profile_Selector{ + { + Entity: minderv1.RepositoryEntity.String(), + Selector: "repository.name == 'testorg/testrepo' || repository.properties.github['is_fork'] != 'true'", + }, + }, + selectorEntityBld: newTestRepoSelectorEntity(), + selected: true, + }, + { + name: "Attempt to use a property but explicitly tell Select that it's not defined", + exprs: []*minderv1.Profile_Selector{ + { + Entity: minderv1.RepositoryEntity.String(), + Selector: "repository.properties.github['is_fork'] != 'true'", + }, + }, + selectOptions: []SelectOption{ + WithUnknownPaths("repository.properties"), + }, + selectorEntityBld: newTestRepoSelectorEntity( + withProperties(map[string]any{ + "github": map[string]any{"is_fork": true}, + }), + ), + expectedSelectErr: ErrResultUnknown, + selected: false, + }, + } + + for _, scenario := range scenarios { + t.Run(scenario.name, func(t *testing.T) { + t.Parallel() + + env, err := NewEnv() + require.NoError(t, err) + + se := scenario.selectorEntityBld() + + sels, err := env.NewSelectionFromProfile(se.EntityType, scenario.exprs) + if scenario.expectedNewSelectionErr != "" { + require.Error(t, err) + require.Contains(t, err.Error(), scenario.expectedNewSelectionErr) + return + } + + require.NoError(t, err) + require.NotNil(t, sels) + + selected, err := sels.Select(se, scenario.selectOptions...) + if scenario.expectedSelectErr != nil { + require.Error(t, err) + require.Equal(t, scenario.expectedSelectErr, err) + return + } + + require.NoError(t, err) + require.Equal(t, scenario.selected, selected) + }) + } +} + +func TestSelectorEntityFillProperties(t *testing.T) { + t.Parallel() + + scenarios := []struct { + name string + exprs []*minderv1.Profile_Selector + mockFetch func(*internalpb.SelectorEntity) + secondSucceeds bool + }{ + { + name: "Fetch a property that exists", + exprs: []*minderv1.Profile_Selector{ + { + Entity: minderv1.RepositoryEntity.String(), + Selector: "repository.properties.github['is_fork'] == false", + }, + }, + mockFetch: func(se *internalpb.SelectorEntity) { + se.Entity.(*internalpb.SelectorEntity_Repository).Repository.Properties = &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "github": { + Kind: &structpb.Value_StructValue{ + StructValue: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "is_fork": { + Kind: &structpb.Value_BoolValue{ + BoolValue: false, + }, + }, + }, + }, + }, + }, + }} + }, + secondSucceeds: true, + }, + { + name: "Fail to fetch a property", + exprs: []*minderv1.Profile_Selector{ + { + Entity: minderv1.RepositoryEntity.String(), + Selector: "repository.properties.github['is_private'] == false", + }, + }, + mockFetch: func(se *internalpb.SelectorEntity) { + se.Entity.(*internalpb.SelectorEntity_Repository).Repository.Properties = &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "github": { + Kind: &structpb.Value_StructValue{ + StructValue: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "is_fork": { + Kind: &structpb.Value_BoolValue{ + BoolValue: false, + }, + }, + }, + }, + }, + }, + }} + }, + secondSucceeds: false, + }, + } + + for _, scenario := range scenarios { + env, err := NewEnv() + require.NoError(t, err) + + seBuilder := newTestRepoSelectorEntity() + se := seBuilder() + + sels, err := env.NewSelectionFromProfile(se.EntityType, scenario.exprs) + require.NoError(t, err) + require.NotNil(t, sels) + + _, err = sels.Select(se, WithUnknownPaths("repository.properties")) + require.ErrorIs(t, err, ErrResultUnknown) + + // simulate fetching properties + scenario.mockFetch(se) + + selected, err := sels.Select(se) + if scenario.secondSucceeds { + require.NoError(t, err) + require.True(t, selected) + } else { + require.ErrorIs(t, err, ErrResultUnknown) + } + } +} diff --git a/internal/proto/internal.pb.go b/internal/proto/internal.pb.go index b2fb96739b..004cd12eba 100644 --- a/internal/proto/internal.pb.go +++ b/internal/proto/internal.pb.go @@ -27,6 +27,7 @@ import ( v1 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + structpb "google.golang.org/protobuf/types/known/structpb" reflect "reflect" sync "sync" ) @@ -263,6 +264,329 @@ func (x *PrContents) GetFiles() []*PrContents_File { return nil } +type SelectorProvider struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // the name of the provider, e.g. github-app-jakubtestorg + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // the class of the provider, e.g. github-app + Class string `protobuf:"bytes,2,opt,name=class,proto3" json:"class,omitempty"` +} + +func (x *SelectorProvider) Reset() { + *x = SelectorProvider{} + if protoimpl.UnsafeEnabled { + mi := &file_internal_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SelectorProvider) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SelectorProvider) ProtoMessage() {} + +func (x *SelectorProvider) ProtoReflect() protoreflect.Message { + mi := &file_internal_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SelectorProvider.ProtoReflect.Descriptor instead. +func (*SelectorProvider) Descriptor() ([]byte, []int) { + return file_internal_proto_rawDescGZIP(), []int{3} +} + +func (x *SelectorProvider) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SelectorProvider) GetClass() string { + if x != nil { + return x.Class + } + return "" +} + +type SelectorRepository struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // the full name of the repository, e.g. stacklok/minder + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // the provider of the repository + Provider *SelectorProvider `protobuf:"bytes,2,opt,name=provider,proto3" json:"provider,omitempty"` + // is_fork is true if the repository is a fork, nil if "don't know" or rather + // not applicable to this provider + IsFork *bool `protobuf:"varint,3,opt,name=is_fork,json=isFork,proto3,oneof" json:"is_fork,omitempty"` + // is_private is true if the repository is private, nil if "don't know" or rather + // not applicable to this provider + IsPrivate *bool `protobuf:"varint,4,opt,name=is_private,json=isPrivate,proto3,oneof" json:"is_private,omitempty"` + Properties *structpb.Struct `protobuf:"bytes,5,opt,name=properties,proto3" json:"properties,omitempty"` +} + +func (x *SelectorRepository) Reset() { + *x = SelectorRepository{} + if protoimpl.UnsafeEnabled { + mi := &file_internal_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SelectorRepository) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SelectorRepository) ProtoMessage() {} + +func (x *SelectorRepository) ProtoReflect() protoreflect.Message { + mi := &file_internal_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SelectorRepository.ProtoReflect.Descriptor instead. +func (*SelectorRepository) Descriptor() ([]byte, []int) { + return file_internal_proto_rawDescGZIP(), []int{4} +} + +func (x *SelectorRepository) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SelectorRepository) GetProvider() *SelectorProvider { + if x != nil { + return x.Provider + } + return nil +} + +func (x *SelectorRepository) GetIsFork() bool { + if x != nil && x.IsFork != nil { + return *x.IsFork + } + return false +} + +func (x *SelectorRepository) GetIsPrivate() bool { + if x != nil && x.IsPrivate != nil { + return *x.IsPrivate + } + return false +} + +func (x *SelectorRepository) GetProperties() *structpb.Struct { + if x != nil { + return x.Properties + } + return nil +} + +type SelectorArtifact struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // the full name of the artifact, e.g. stacklok/minder-server + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // the provider of the artifact + Provider *SelectorProvider `protobuf:"bytes,2,opt,name=provider,proto3" json:"provider,omitempty"` + // the type of the artifact, e.g. "container" + Type string `protobuf:"bytes,4,opt,name=type,proto3" json:"type,omitempty"` + Properties *structpb.Struct `protobuf:"bytes,5,opt,name=properties,proto3" json:"properties,omitempty"` +} + +func (x *SelectorArtifact) Reset() { + *x = SelectorArtifact{} + if protoimpl.UnsafeEnabled { + mi := &file_internal_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SelectorArtifact) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SelectorArtifact) ProtoMessage() {} + +func (x *SelectorArtifact) ProtoReflect() protoreflect.Message { + mi := &file_internal_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SelectorArtifact.ProtoReflect.Descriptor instead. +func (*SelectorArtifact) Descriptor() ([]byte, []int) { + return file_internal_proto_rawDescGZIP(), []int{5} +} + +func (x *SelectorArtifact) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SelectorArtifact) GetProvider() *SelectorProvider { + if x != nil { + return x.Provider + } + return nil +} + +func (x *SelectorArtifact) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *SelectorArtifact) GetProperties() *structpb.Struct { + if x != nil { + return x.Properties + } + return nil +} + +type SelectorEntity struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // one of repository, pull_request, artifact (see oneof entity) + EntityType v1.Entity `protobuf:"varint,1,opt,name=entity_type,json=entityType,proto3,enum=minder.v1.Entity" json:"entity_type,omitempty"` + // the name of the entity, same as the name in the entity message + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Provider *SelectorProvider `protobuf:"bytes,3,opt,name=provider,proto3" json:"provider,omitempty"` + // Types that are assignable to Entity: + // + // *SelectorEntity_Repository + // *SelectorEntity_Artifact + Entity isSelectorEntity_Entity `protobuf_oneof:"entity"` +} + +func (x *SelectorEntity) Reset() { + *x = SelectorEntity{} + if protoimpl.UnsafeEnabled { + mi := &file_internal_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SelectorEntity) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SelectorEntity) ProtoMessage() {} + +func (x *SelectorEntity) ProtoReflect() protoreflect.Message { + mi := &file_internal_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SelectorEntity.ProtoReflect.Descriptor instead. +func (*SelectorEntity) Descriptor() ([]byte, []int) { + return file_internal_proto_rawDescGZIP(), []int{6} +} + +func (x *SelectorEntity) GetEntityType() v1.Entity { + if x != nil { + return x.EntityType + } + return v1.Entity(0) +} + +func (x *SelectorEntity) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SelectorEntity) GetProvider() *SelectorProvider { + if x != nil { + return x.Provider + } + return nil +} + +func (m *SelectorEntity) GetEntity() isSelectorEntity_Entity { + if m != nil { + return m.Entity + } + return nil +} + +func (x *SelectorEntity) GetRepository() *SelectorRepository { + if x, ok := x.GetEntity().(*SelectorEntity_Repository); ok { + return x.Repository + } + return nil +} + +func (x *SelectorEntity) GetArtifact() *SelectorArtifact { + if x, ok := x.GetEntity().(*SelectorEntity_Artifact); ok { + return x.Artifact + } + return nil +} + +type isSelectorEntity_Entity interface { + isSelectorEntity_Entity() +} + +type SelectorEntity_Repository struct { + Repository *SelectorRepository `protobuf:"bytes,4,opt,name=repository,proto3,oneof"` +} + +type SelectorEntity_Artifact struct { + Artifact *SelectorArtifact `protobuf:"bytes,5,opt,name=artifact,proto3,oneof"` // TODO(jakub): add pull request, too - what would it contain? Just properties? +} + +func (*SelectorEntity_Repository) isSelectorEntity_Entity() {} + +func (*SelectorEntity_Artifact) isSelectorEntity_Entity() {} + type PrDependencies_ContextualDependency struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -275,7 +599,7 @@ type PrDependencies_ContextualDependency struct { func (x *PrDependencies_ContextualDependency) Reset() { *x = PrDependencies_ContextualDependency{} if protoimpl.UnsafeEnabled { - mi := &file_internal_proto_msgTypes[3] + mi := &file_internal_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -288,7 +612,7 @@ func (x *PrDependencies_ContextualDependency) String() string { func (*PrDependencies_ContextualDependency) ProtoMessage() {} func (x *PrDependencies_ContextualDependency) ProtoReflect() protoreflect.Message { - mi := &file_internal_proto_msgTypes[3] + mi := &file_internal_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -330,7 +654,7 @@ type PrDependencies_ContextualDependency_FilePatch struct { func (x *PrDependencies_ContextualDependency_FilePatch) Reset() { *x = PrDependencies_ContextualDependency_FilePatch{} if protoimpl.UnsafeEnabled { - mi := &file_internal_proto_msgTypes[4] + mi := &file_internal_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -343,7 +667,7 @@ func (x *PrDependencies_ContextualDependency_FilePatch) String() string { func (*PrDependencies_ContextualDependency_FilePatch) ProtoMessage() {} func (x *PrDependencies_ContextualDependency_FilePatch) ProtoReflect() protoreflect.Message { - mi := &file_internal_proto_msgTypes[4] + mi := &file_internal_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -386,7 +710,7 @@ type PrContents_File struct { func (x *PrContents_File) Reset() { *x = PrContents_File{} if protoimpl.UnsafeEnabled { - mi := &file_internal_proto_msgTypes[5] + mi := &file_internal_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -399,7 +723,7 @@ func (x *PrContents_File) String() string { func (*PrContents_File) ProtoMessage() {} func (x *PrContents_File) ProtoReflect() protoreflect.Message { - mi := &file_internal_proto_msgTypes[5] + mi := &file_internal_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -450,7 +774,7 @@ type PrContents_File_Line struct { func (x *PrContents_File_Line) Reset() { *x = PrContents_File_Line{} if protoimpl.UnsafeEnabled { - mi := &file_internal_proto_msgTypes[6] + mi := &file_internal_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -463,7 +787,7 @@ func (x *PrContents_File_Line) String() string { func (*PrContents_File_Line) ProtoMessage() {} func (x *PrContents_File_Line) ProtoReflect() protoreflect.Message { - mi := &file_internal_proto_msgTypes[6] + mi := &file_internal_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -499,64 +823,113 @@ var file_internal_proto_rawDesc = []byte{ 0x0a, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x1a, 0x16, 0x6d, 0x69, 0x6e, 0x64, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x69, 0x6e, 0x64, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x22, 0x70, 0x0a, 0x0a, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, - 0x12, 0x34, 0x0a, 0x09, 0x65, 0x63, 0x6f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x44, - 0x65, 0x70, 0x45, 0x63, 0x6f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x09, 0x65, 0x63, 0x6f, - 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x22, 0xc7, 0x02, 0x0a, 0x0e, 0x50, 0x72, 0x44, 0x65, 0x70, 0x65, 0x6e, - 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x12, 0x26, 0x0a, 0x02, 0x70, 0x72, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x69, 0x6e, 0x64, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, - 0x50, 0x75, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x02, 0x70, 0x72, 0x12, - 0x41, 0x0a, 0x04, 0x64, 0x65, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x50, 0x72, 0x44, 0x65, 0x70, 0x65, 0x6e, - 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x75, - 0x61, 0x6c, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x52, 0x04, 0x64, 0x65, - 0x70, 0x73, 0x1a, 0xc9, 0x01, 0x0a, 0x14, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x75, 0x61, - 0x6c, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x26, 0x0a, 0x03, 0x64, - 0x65, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x2e, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x52, 0x03, - 0x64, 0x65, 0x70, 0x12, 0x4b, 0x0a, 0x04, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x37, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x50, 0x72, 0x44, - 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x43, 0x6f, 0x6e, 0x74, - 0x65, 0x78, 0x74, 0x75, 0x61, 0x6c, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, - 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x63, 0x68, 0x52, 0x04, 0x66, 0x69, 0x6c, 0x65, - 0x1a, 0x3c, 0x0a, 0x09, 0x46, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x63, 0x68, 0x12, 0x12, 0x0a, + 0x74, 0x6f, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x22, 0x70, 0x0a, 0x0a, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x34, + 0x0a, 0x09, 0x65, 0x63, 0x6f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x16, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x44, 0x65, 0x70, + 0x45, 0x63, 0x6f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x52, 0x09, 0x65, 0x63, 0x6f, 0x73, 0x79, + 0x73, 0x74, 0x65, 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x22, 0xc7, 0x02, 0x0a, 0x0e, 0x50, 0x72, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, + 0x6e, 0x63, 0x69, 0x65, 0x73, 0x12, 0x26, 0x0a, 0x02, 0x70, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x69, 0x6e, 0x64, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x75, + 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x02, 0x70, 0x72, 0x12, 0x41, 0x0a, + 0x04, 0x64, 0x65, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x50, 0x72, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, + 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x75, 0x61, 0x6c, + 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x52, 0x04, 0x64, 0x65, 0x70, 0x73, + 0x1a, 0xc9, 0x01, 0x0a, 0x14, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x75, 0x61, 0x6c, 0x44, + 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x26, 0x0a, 0x03, 0x64, 0x65, 0x70, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2e, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x52, 0x03, 0x64, 0x65, + 0x70, 0x12, 0x4b, 0x0a, 0x04, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x37, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x50, 0x72, 0x44, 0x65, 0x70, + 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x78, + 0x74, 0x75, 0x61, 0x6c, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x2e, 0x46, + 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x63, 0x68, 0x52, 0x04, 0x66, 0x69, 0x6c, 0x65, 0x1a, 0x3c, + 0x0a, 0x09, 0x46, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x63, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, + 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x74, 0x63, 0x68, 0x55, 0x72, 0x6c, 0x22, 0xac, 0x02, 0x0a, + 0x0a, 0x50, 0x72, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x26, 0x0a, 0x02, 0x70, + 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x69, 0x6e, 0x64, 0x65, 0x72, + 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x75, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, + 0x02, 0x70, 0x72, 0x12, 0x2f, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x50, 0x72, + 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x66, + 0x69, 0x6c, 0x65, 0x73, 0x1a, 0xc4, 0x01, 0x0a, 0x04, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x74, 0x63, 0x68, 0x55, 0x72, 0x6c, 0x22, 0xac, - 0x02, 0x0a, 0x0a, 0x50, 0x72, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x26, 0x0a, - 0x02, 0x70, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x69, 0x6e, 0x64, - 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x75, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x52, 0x02, 0x70, 0x72, 0x12, 0x2f, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x02, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, - 0x50, 0x72, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x52, - 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x1a, 0xc4, 0x01, 0x0a, 0x04, 0x46, 0x69, 0x6c, 0x65, 0x12, + 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x63, 0x68, 0x5f, + 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x66, 0x69, 0x6c, 0x65, 0x50, + 0x61, 0x74, 0x63, 0x68, 0x55, 0x72, 0x6c, 0x12, 0x3f, 0x0a, 0x0b, 0x70, 0x61, 0x74, 0x63, 0x68, + 0x5f, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x50, 0x72, 0x43, 0x6f, 0x6e, 0x74, 0x65, 0x6e, + 0x74, 0x73, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x4c, 0x69, 0x6e, 0x65, 0x52, 0x0a, 0x70, 0x61, + 0x74, 0x63, 0x68, 0x4c, 0x69, 0x6e, 0x65, 0x73, 0x1a, 0x41, 0x0a, 0x04, 0x4c, 0x69, 0x6e, 0x65, + 0x12, 0x1f, 0x0a, 0x0b, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x6c, 0x69, 0x6e, 0x65, 0x4e, 0x75, 0x6d, 0x62, 0x65, + 0x72, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x22, 0x3c, 0x0a, 0x10, 0x53, + 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x63, - 0x68, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x66, 0x69, 0x6c, - 0x65, 0x50, 0x61, 0x74, 0x63, 0x68, 0x55, 0x72, 0x6c, 0x12, 0x3f, 0x0a, 0x0b, 0x70, 0x61, 0x74, - 0x63, 0x68, 0x5f, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, - 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x50, 0x72, 0x43, 0x6f, 0x6e, 0x74, - 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x4c, 0x69, 0x6e, 0x65, 0x52, 0x0a, - 0x70, 0x61, 0x74, 0x63, 0x68, 0x4c, 0x69, 0x6e, 0x65, 0x73, 0x1a, 0x41, 0x0a, 0x04, 0x4c, 0x69, - 0x6e, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x6c, 0x69, 0x6e, 0x65, 0x4e, 0x75, 0x6d, - 0x62, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2a, 0x72, 0x0a, - 0x0c, 0x44, 0x65, 0x70, 0x45, 0x63, 0x6f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x1d, 0x0a, - 0x19, 0x44, 0x45, 0x50, 0x5f, 0x45, 0x43, 0x4f, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x5f, 0x55, - 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11, - 0x44, 0x45, 0x50, 0x5f, 0x45, 0x43, 0x4f, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x5f, 0x4e, 0x50, - 0x4d, 0x10, 0x01, 0x12, 0x14, 0x0a, 0x10, 0x44, 0x45, 0x50, 0x5f, 0x45, 0x43, 0x4f, 0x53, 0x59, - 0x53, 0x54, 0x45, 0x4d, 0x5f, 0x47, 0x4f, 0x10, 0x02, 0x12, 0x16, 0x0a, 0x12, 0x44, 0x45, 0x50, - 0x5f, 0x45, 0x43, 0x4f, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x5f, 0x50, 0x59, 0x50, 0x49, 0x10, - 0x03, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x73, 0x74, 0x61, 0x63, 0x6b, 0x6c, 0x6f, 0x6b, 0x2f, 0x6d, 0x69, 0x6e, 0x64, 0x65, 0x72, 0x2f, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x22, 0xf6, 0x01, 0x0a, 0x12, 0x53, 0x65, + 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, + 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x36, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x07, + 0x69, 0x73, 0x5f, 0x66, 0x6f, 0x72, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, + 0x06, 0x69, 0x73, 0x46, 0x6f, 0x72, 0x6b, 0x88, 0x01, 0x01, 0x12, 0x22, 0x0a, 0x0a, 0x69, 0x73, + 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x01, + 0x52, 0x09, 0x69, 0x73, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x88, 0x01, 0x01, 0x12, 0x37, + 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x0a, 0x70, 0x72, 0x6f, + 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x69, 0x73, 0x5f, 0x66, + 0x6f, 0x72, 0x6b, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x69, 0x73, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x22, 0xab, 0x01, 0x0a, 0x10, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x41, + 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x36, 0x0a, 0x08, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x37, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x65, + 0x72, 0x74, 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, + 0x72, 0x75, 0x63, 0x74, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x69, 0x65, 0x73, + 0x22, 0x94, 0x02, 0x0a, 0x0e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x45, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x12, 0x32, 0x0a, 0x0b, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x6d, 0x69, 0x6e, 0x64, 0x65, + 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x0a, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x36, 0x0a, 0x08, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x12, 0x3e, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, + 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x70, 0x6f, 0x73, + 0x69, 0x74, 0x6f, 0x72, 0x79, 0x48, 0x00, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x6f, 0x72, 0x79, 0x12, 0x38, 0x0a, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, + 0x74, 0x48, 0x00, 0x52, 0x08, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x42, 0x08, 0x0a, + 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2a, 0x72, 0x0a, 0x0c, 0x44, 0x65, 0x70, 0x45, 0x63, + 0x6f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x1d, 0x0a, 0x19, 0x44, 0x45, 0x50, 0x5f, 0x45, + 0x43, 0x4f, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, + 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11, 0x44, 0x45, 0x50, 0x5f, 0x45, 0x43, + 0x4f, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x5f, 0x4e, 0x50, 0x4d, 0x10, 0x01, 0x12, 0x14, 0x0a, + 0x10, 0x44, 0x45, 0x50, 0x5f, 0x45, 0x43, 0x4f, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x5f, 0x47, + 0x4f, 0x10, 0x02, 0x12, 0x16, 0x0a, 0x12, 0x44, 0x45, 0x50, 0x5f, 0x45, 0x43, 0x4f, 0x53, 0x59, + 0x53, 0x54, 0x45, 0x4d, 0x5f, 0x50, 0x59, 0x50, 0x49, 0x10, 0x03, 0x42, 0x2b, 0x5a, 0x29, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x6c, + 0x6f, 0x6b, 0x2f, 0x6d, 0x69, 0x6e, 0x64, 0x65, 0x72, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -572,32 +945,46 @@ func file_internal_proto_rawDescGZIP() []byte { } var file_internal_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_internal_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_internal_proto_msgTypes = make([]protoimpl.MessageInfo, 11) var file_internal_proto_goTypes = []any{ (DepEcosystem)(0), // 0: internal.DepEcosystem (*Dependency)(nil), // 1: internal.Dependency (*PrDependencies)(nil), // 2: internal.PrDependencies (*PrContents)(nil), // 3: internal.PrContents - (*PrDependencies_ContextualDependency)(nil), // 4: internal.PrDependencies.ContextualDependency - (*PrDependencies_ContextualDependency_FilePatch)(nil), // 5: internal.PrDependencies.ContextualDependency.FilePatch - (*PrContents_File)(nil), // 6: internal.PrContents.File - (*PrContents_File_Line)(nil), // 7: internal.PrContents.File.Line - (*v1.PullRequest)(nil), // 8: minder.v1.PullRequest + (*SelectorProvider)(nil), // 4: internal.SelectorProvider + (*SelectorRepository)(nil), // 5: internal.SelectorRepository + (*SelectorArtifact)(nil), // 6: internal.SelectorArtifact + (*SelectorEntity)(nil), // 7: internal.SelectorEntity + (*PrDependencies_ContextualDependency)(nil), // 8: internal.PrDependencies.ContextualDependency + (*PrDependencies_ContextualDependency_FilePatch)(nil), // 9: internal.PrDependencies.ContextualDependency.FilePatch + (*PrContents_File)(nil), // 10: internal.PrContents.File + (*PrContents_File_Line)(nil), // 11: internal.PrContents.File.Line + (*v1.PullRequest)(nil), // 12: minder.v1.PullRequest + (*structpb.Struct)(nil), // 13: google.protobuf.Struct + (v1.Entity)(0), // 14: minder.v1.Entity } var file_internal_proto_depIdxs = []int32{ - 0, // 0: internal.Dependency.ecosystem:type_name -> internal.DepEcosystem - 8, // 1: internal.PrDependencies.pr:type_name -> minder.v1.PullRequest - 4, // 2: internal.PrDependencies.deps:type_name -> internal.PrDependencies.ContextualDependency - 8, // 3: internal.PrContents.pr:type_name -> minder.v1.PullRequest - 6, // 4: internal.PrContents.files:type_name -> internal.PrContents.File - 1, // 5: internal.PrDependencies.ContextualDependency.dep:type_name -> internal.Dependency - 5, // 6: internal.PrDependencies.ContextualDependency.file:type_name -> internal.PrDependencies.ContextualDependency.FilePatch - 7, // 7: internal.PrContents.File.patch_lines:type_name -> internal.PrContents.File.Line - 8, // [8:8] is the sub-list for method output_type - 8, // [8:8] is the sub-list for method input_type - 8, // [8:8] is the sub-list for extension type_name - 8, // [8:8] is the sub-list for extension extendee - 0, // [0:8] is the sub-list for field type_name + 0, // 0: internal.Dependency.ecosystem:type_name -> internal.DepEcosystem + 12, // 1: internal.PrDependencies.pr:type_name -> minder.v1.PullRequest + 8, // 2: internal.PrDependencies.deps:type_name -> internal.PrDependencies.ContextualDependency + 12, // 3: internal.PrContents.pr:type_name -> minder.v1.PullRequest + 10, // 4: internal.PrContents.files:type_name -> internal.PrContents.File + 4, // 5: internal.SelectorRepository.provider:type_name -> internal.SelectorProvider + 13, // 6: internal.SelectorRepository.properties:type_name -> google.protobuf.Struct + 4, // 7: internal.SelectorArtifact.provider:type_name -> internal.SelectorProvider + 13, // 8: internal.SelectorArtifact.properties:type_name -> google.protobuf.Struct + 14, // 9: internal.SelectorEntity.entity_type:type_name -> minder.v1.Entity + 4, // 10: internal.SelectorEntity.provider:type_name -> internal.SelectorProvider + 5, // 11: internal.SelectorEntity.repository:type_name -> internal.SelectorRepository + 6, // 12: internal.SelectorEntity.artifact:type_name -> internal.SelectorArtifact + 1, // 13: internal.PrDependencies.ContextualDependency.dep:type_name -> internal.Dependency + 9, // 14: internal.PrDependencies.ContextualDependency.file:type_name -> internal.PrDependencies.ContextualDependency.FilePatch + 11, // 15: internal.PrContents.File.patch_lines:type_name -> internal.PrContents.File.Line + 16, // [16:16] is the sub-list for method output_type + 16, // [16:16] is the sub-list for method input_type + 16, // [16:16] is the sub-list for extension type_name + 16, // [16:16] is the sub-list for extension extendee + 0, // [0:16] is the sub-list for field type_name } func init() { file_internal_proto_init() } @@ -643,7 +1030,7 @@ func file_internal_proto_init() { } } file_internal_proto_msgTypes[3].Exporter = func(v any, i int) any { - switch v := v.(*PrDependencies_ContextualDependency); i { + switch v := v.(*SelectorProvider); i { case 0: return &v.state case 1: @@ -655,7 +1042,7 @@ func file_internal_proto_init() { } } file_internal_proto_msgTypes[4].Exporter = func(v any, i int) any { - switch v := v.(*PrDependencies_ContextualDependency_FilePatch); i { + switch v := v.(*SelectorRepository); i { case 0: return &v.state case 1: @@ -667,7 +1054,7 @@ func file_internal_proto_init() { } } file_internal_proto_msgTypes[5].Exporter = func(v any, i int) any { - switch v := v.(*PrContents_File); i { + switch v := v.(*SelectorArtifact); i { case 0: return &v.state case 1: @@ -679,6 +1066,54 @@ func file_internal_proto_init() { } } file_internal_proto_msgTypes[6].Exporter = func(v any, i int) any { + switch v := v.(*SelectorEntity); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_internal_proto_msgTypes[7].Exporter = func(v any, i int) any { + switch v := v.(*PrDependencies_ContextualDependency); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_internal_proto_msgTypes[8].Exporter = func(v any, i int) any { + switch v := v.(*PrDependencies_ContextualDependency_FilePatch); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_internal_proto_msgTypes[9].Exporter = func(v any, i int) any { + switch v := v.(*PrContents_File); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_internal_proto_msgTypes[10].Exporter = func(v any, i int) any { switch v := v.(*PrContents_File_Line); i { case 0: return &v.state @@ -691,13 +1126,18 @@ func file_internal_proto_init() { } } } + file_internal_proto_msgTypes[4].OneofWrappers = []any{} + file_internal_proto_msgTypes[6].OneofWrappers = []any{ + (*SelectorEntity_Repository)(nil), + (*SelectorEntity_Artifact)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_internal_proto_rawDesc, NumEnums: 1, - NumMessages: 7, + NumMessages: 11, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/proto/internal.proto b/internal/proto/internal.proto index 771858e7a9..f557684a18 100644 --- a/internal/proto/internal.proto +++ b/internal/proto/internal.proto @@ -19,6 +19,7 @@ syntax = "proto3"; package internal; import "minder/v1/minder.proto"; +import "google/protobuf/struct.proto"; option go_package = "github.com/stacklok/minder/internal/proto"; @@ -67,4 +68,53 @@ message PrContents { minder.v1.PullRequest pr = 1; repeated File files = 2; -} \ No newline at end of file +} + +message SelectorProvider { + // the name of the provider, e.g. github-app-jakubtestorg + string name = 1; + // the class of the provider, e.g. github-app + string class = 2; +} + +message SelectorRepository { + // the full name of the repository, e.g. stacklok/minder + string name = 1; + // the provider of the repository + SelectorProvider provider = 2; + + // is_fork is true if the repository is a fork, nil if "don't know" or rather + // not applicable to this provider + optional bool is_fork = 3; + // is_private is true if the repository is private, nil if "don't know" or rather + // not applicable to this provider + optional bool is_private = 4; + + google.protobuf.Struct properties = 5; +} + +message SelectorArtifact { + // the full name of the artifact, e.g. stacklok/minder-server + string name = 1; + // the provider of the artifact + SelectorProvider provider = 2; + + // the type of the artifact, e.g. "container" + string type = 4; + + google.protobuf.Struct properties = 5; +} + +message SelectorEntity { + // one of repository, pull_request, artifact (see oneof entity) + minder.v1.Entity entity_type = 1; + // the name of the entity, same as the name in the entity message + string name = 2; + SelectorProvider provider = 3; + + oneof entity { + SelectorRepository repository = 4; + SelectorArtifact artifact = 5; + // TODO(jakub): add pull request, too - what would it contain? Just properties? + } +} diff --git a/internal/providers/github/selector_entity.go b/internal/providers/github/selector_entity.go new file mode 100644 index 0000000000..16a958e16a --- /dev/null +++ b/internal/providers/github/selector_entity.go @@ -0,0 +1,59 @@ +// +// Copyright 2024 Stacklok, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package github + +import ( + "context" + "fmt" + + "google.golang.org/protobuf/proto" + + internalpb "github.com/stacklok/minder/internal/proto" + minderv1 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1" +) + +// RepoToSelectorEntity converts a Repository to a SelectorEntity +func (_ *GitHub) RepoToSelectorEntity(_ context.Context, r *minderv1.Repository) *internalpb.SelectorEntity { + fullName := fmt.Sprintf("%s/%s", r.GetOwner(), r.GetName()) + + return &internalpb.SelectorEntity{ + EntityType: minderv1.Entity_ENTITY_REPOSITORIES, + Name: fullName, + Entity: &internalpb.SelectorEntity_Repository{ + Repository: &internalpb.SelectorRepository{ + Name: fullName, + IsFork: proto.Bool(r.GetIsFork()), + IsPrivate: proto.Bool(r.GetIsPrivate()), + }, + }, + } +} + +// ArtifactToSelectorEntity converts an Artifact to a SelectorEntity +func (_ *GitHub) ArtifactToSelectorEntity(_ context.Context, a *minderv1.Artifact) *internalpb.SelectorEntity { + fullName := fmt.Sprintf("%s/%s", a.GetOwner(), a.GetName()) + + return &internalpb.SelectorEntity{ + EntityType: minderv1.Entity_ENTITY_ARTIFACTS, + Name: fullName, + Entity: &internalpb.SelectorEntity_Artifact{ + Artifact: &internalpb.SelectorArtifact{ + Name: fullName, + Type: a.GetType(), + }, + }, + } +} diff --git a/internal/providers/selectors/interface.go b/internal/providers/selectors/interface.go new file mode 100644 index 0000000000..364e6d967c --- /dev/null +++ b/internal/providers/selectors/interface.go @@ -0,0 +1,43 @@ +// +// Copyright 2024 Stacklok, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package selectors provides utilities to convert entities to selector entities. +package selectors + +import ( + "context" + + internalpb "github.com/stacklok/minder/internal/proto" + minderv1 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1" + provifv1 "github.com/stacklok/minder/pkg/providers/v1" +) + +//go:generate go run go.uber.org/mock/mockgen -package mock_$GOPACKAGE -destination=./mock/$GOFILE -source=./$GOFILE + +// RepoSelectorConverter is an interface for converting a repository to a repository selector +type RepoSelectorConverter interface { + provifv1.Provider + + // RepoToSelectorEntity converts the given repository to a repository selector + RepoToSelectorEntity(ctx context.Context, repo *minderv1.Repository) *internalpb.SelectorEntity +} + +// ArtifactSelectorConverter is an interface for converting an artifact to a artifact selector +type ArtifactSelectorConverter interface { + provifv1.Provider + + // ArtifactToSelectorEntity converts the given artifact to a artifact selector + ArtifactToSelectorEntity(ctx context.Context, artifact *minderv1.Artifact) *internalpb.SelectorEntity +} diff --git a/internal/providers/selectors/mock/interface.go b/internal/providers/selectors/mock/interface.go new file mode 100644 index 0000000000..6bffd1ccbf --- /dev/null +++ b/internal/providers/selectors/mock/interface.go @@ -0,0 +1,121 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./interface.go +// +// Generated by this command: +// +// mockgen -package mock_selectors -destination=./mock/interface.go -source=./interface.go +// + +// Package mock_selectors is a generated GoMock package. +package mock_selectors + +import ( + context "context" + reflect "reflect" + + proto "github.com/stacklok/minder/internal/proto" + v1 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1" + gomock "go.uber.org/mock/gomock" +) + +// MockRepoSelectorConverter is a mock of RepoSelectorConverter interface. +type MockRepoSelectorConverter struct { + ctrl *gomock.Controller + recorder *MockRepoSelectorConverterMockRecorder +} + +// MockRepoSelectorConverterMockRecorder is the mock recorder for MockRepoSelectorConverter. +type MockRepoSelectorConverterMockRecorder struct { + mock *MockRepoSelectorConverter +} + +// NewMockRepoSelectorConverter creates a new mock instance. +func NewMockRepoSelectorConverter(ctrl *gomock.Controller) *MockRepoSelectorConverter { + mock := &MockRepoSelectorConverter{ctrl: ctrl} + mock.recorder = &MockRepoSelectorConverterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepoSelectorConverter) EXPECT() *MockRepoSelectorConverterMockRecorder { + return m.recorder +} + +// CanImplement mocks base method. +func (m *MockRepoSelectorConverter) CanImplement(trait v1.ProviderType) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CanImplement", trait) + ret0, _ := ret[0].(bool) + return ret0 +} + +// CanImplement indicates an expected call of CanImplement. +func (mr *MockRepoSelectorConverterMockRecorder) CanImplement(trait any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CanImplement", reflect.TypeOf((*MockRepoSelectorConverter)(nil).CanImplement), trait) +} + +// RepoToSelectorEntity mocks base method. +func (m *MockRepoSelectorConverter) RepoToSelectorEntity(ctx context.Context, repo *v1.Repository) *proto.SelectorEntity { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RepoToSelectorEntity", ctx, repo) + ret0, _ := ret[0].(*proto.SelectorEntity) + return ret0 +} + +// RepoToSelectorEntity indicates an expected call of RepoToSelectorEntity. +func (mr *MockRepoSelectorConverterMockRecorder) RepoToSelectorEntity(ctx, repo any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RepoToSelectorEntity", reflect.TypeOf((*MockRepoSelectorConverter)(nil).RepoToSelectorEntity), ctx, repo) +} + +// MockArtifactSelectorConverter is a mock of ArtifactSelectorConverter interface. +type MockArtifactSelectorConverter struct { + ctrl *gomock.Controller + recorder *MockArtifactSelectorConverterMockRecorder +} + +// MockArtifactSelectorConverterMockRecorder is the mock recorder for MockArtifactSelectorConverter. +type MockArtifactSelectorConverterMockRecorder struct { + mock *MockArtifactSelectorConverter +} + +// NewMockArtifactSelectorConverter creates a new mock instance. +func NewMockArtifactSelectorConverter(ctrl *gomock.Controller) *MockArtifactSelectorConverter { + mock := &MockArtifactSelectorConverter{ctrl: ctrl} + mock.recorder = &MockArtifactSelectorConverterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockArtifactSelectorConverter) EXPECT() *MockArtifactSelectorConverterMockRecorder { + return m.recorder +} + +// ArtifactToSelectorEntity mocks base method. +func (m *MockArtifactSelectorConverter) ArtifactToSelectorEntity(ctx context.Context, artifact *v1.Artifact) *proto.SelectorEntity { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ArtifactToSelectorEntity", ctx, artifact) + ret0, _ := ret[0].(*proto.SelectorEntity) + return ret0 +} + +// ArtifactToSelectorEntity indicates an expected call of ArtifactToSelectorEntity. +func (mr *MockArtifactSelectorConverterMockRecorder) ArtifactToSelectorEntity(ctx, artifact any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ArtifactToSelectorEntity", reflect.TypeOf((*MockArtifactSelectorConverter)(nil).ArtifactToSelectorEntity), ctx, artifact) +} + +// CanImplement mocks base method. +func (m *MockArtifactSelectorConverter) CanImplement(trait v1.ProviderType) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CanImplement", trait) + ret0, _ := ret[0].(bool) + return ret0 +} + +// CanImplement indicates an expected call of CanImplement. +func (mr *MockArtifactSelectorConverterMockRecorder) CanImplement(trait any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CanImplement", reflect.TypeOf((*MockArtifactSelectorConverter)(nil).CanImplement), trait) +} diff --git a/internal/providers/selectors/selector_entity.go b/internal/providers/selectors/selector_entity.go new file mode 100644 index 0000000000..8de5a95995 --- /dev/null +++ b/internal/providers/selectors/selector_entity.go @@ -0,0 +1,127 @@ +// +// Copyright 2024 Stacklok, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package selectors + +import ( + "context" + "fmt" + + "google.golang.org/protobuf/proto" + + internalpb "github.com/stacklok/minder/internal/proto" + minderv1 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1" + provifv1 "github.com/stacklok/minder/pkg/providers/v1" +) + +// entityInfoConverter is an interface for converting an entity from an EntityInfoWrapper to a SelectorEntity +type entityInfoConverter interface { + toSelectorEntity(ctx context.Context, entity proto.Message) *internalpb.SelectorEntity +} + +type repositoryInfoConverter struct { + converter RepoSelectorConverter +} + +func newRepositoryInfoConverter(provider provifv1.Provider) *repositoryInfoConverter { + converter, err := provifv1.As[RepoSelectorConverter](provider) + if err != nil { + return nil + } + + return &repositoryInfoConverter{ + converter: converter, + } +} + +func (rc *repositoryInfoConverter) toSelectorEntity(ctx context.Context, entity proto.Message) *internalpb.SelectorEntity { + if rc == nil { + return nil + } + + r, ok := entity.(*minderv1.Repository) + if !ok { + return nil + } + + return rc.converter.RepoToSelectorEntity(ctx, r) +} + +type artifactInfoConverter struct { + converter ArtifactSelectorConverter +} + +func newArtifactInfoConverter(provider provifv1.Provider) *artifactInfoConverter { + converter, err := provifv1.As[ArtifactSelectorConverter](provider) + if err != nil { + return nil + } + + return &artifactInfoConverter{ + converter: converter, + } +} + +func (ac *artifactInfoConverter) toSelectorEntity(ctx context.Context, entity proto.Message) *internalpb.SelectorEntity { + if ac == nil { + return nil + } + + a, ok := entity.(*minderv1.Artifact) + if !ok { + return nil + } + + return ac.converter.ArtifactToSelectorEntity(ctx, a) +} + +// converterFactory is a map of entity types to their respective converters +type converterFactory struct { + converters map[minderv1.Entity]entityInfoConverter +} + +// newConverterFactory creates a new converterFactory with the default converters for each entity type +func newConverterFactory(provider provifv1.Provider) *converterFactory { + return &converterFactory{ + converters: map[minderv1.Entity]entityInfoConverter{ + minderv1.Entity_ENTITY_REPOSITORIES: newRepositoryInfoConverter(provider), + minderv1.Entity_ENTITY_ARTIFACTS: newArtifactInfoConverter(provider), + }, + } +} + +func (cf *converterFactory) getConverter(entityType minderv1.Entity) (entityInfoConverter, error) { + conv, ok := cf.converters[entityType] + if !ok { + return nil, fmt.Errorf("no converter found for entity type %v", entityType) + } + + return conv, nil +} + +// EntityToSelectorEntity converts an entity to a SelectorEntity +func EntityToSelectorEntity( + ctx context.Context, + provider provifv1.Provider, + entType minderv1.Entity, + entity proto.Message, +) *internalpb.SelectorEntity { + factory := newConverterFactory(provider) + conv, err := factory.getConverter(entType) + if err != nil { + return nil + } + return conv.toSelectorEntity(ctx, entity) +} diff --git a/internal/providers/selectors/selector_entity_test.go b/internal/providers/selectors/selector_entity_test.go new file mode 100644 index 0000000000..b0b7f513cd --- /dev/null +++ b/internal/providers/selectors/selector_entity_test.go @@ -0,0 +1,208 @@ +// +// Copyright 2024 Stacklok, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package selectors + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + + internalpb "github.com/stacklok/minder/internal/proto" + minderv1 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1" + provifv1 "github.com/stacklok/minder/pkg/providers/v1" +) + +func repoToSelectorEntity(t *testing.T, name, class string, repo *minderv1.Repository) *internalpb.SelectorEntity { + t.Helper() + + return &internalpb.SelectorEntity{ + EntityType: minderv1.Entity_ENTITY_REPOSITORIES, + Name: fmt.Sprintf("%s/%s", repo.GetOwner(), repo.GetName()), + Provider: &internalpb.SelectorProvider{ + Name: name, + Class: class, + }, + Entity: &internalpb.SelectorEntity_Repository{ + Repository: &internalpb.SelectorRepository{ + Name: fmt.Sprintf("%s/%s", repo.GetOwner(), repo.GetName()), + Provider: &internalpb.SelectorProvider{ + Name: name, + Class: class, + }, + IsFork: proto.Bool(repo.GetIsFork()), + IsPrivate: proto.Bool(repo.GetIsFork()), + }, + }, + } +} + +func artifactToSelectorEntity(t *testing.T, name, class string, artifact *minderv1.Artifact) *internalpb.SelectorEntity { + t.Helper() + + return &internalpb.SelectorEntity{ + EntityType: minderv1.Entity_ENTITY_ARTIFACTS, + Name: fmt.Sprintf("%s/%s", artifact.GetOwner(), artifact.GetName()), + Provider: &internalpb.SelectorProvider{ + Name: name, + Class: class, + }, + Entity: &internalpb.SelectorEntity_Artifact{ + Artifact: &internalpb.SelectorArtifact{ + Name: fmt.Sprintf("%s/%s", artifact.GetOwner(), artifact.GetName()), + Provider: &internalpb.SelectorProvider{ + Name: name, + Class: class, + }, + }, + }, + } +} + +type fullProvider struct { + name string + class string + t *testing.T +} + +func (_ *fullProvider) CanImplement(_ minderv1.ProviderType) bool { + return true +} + +func (m *fullProvider) RepoToSelectorEntity(_ context.Context, repo *minderv1.Repository) *internalpb.SelectorEntity { + return repoToSelectorEntity(m.t, m.name, m.class, repo) +} + +func (m *fullProvider) ArtifactToSelectorEntity(_ context.Context, artifact *minderv1.Artifact) *internalpb.SelectorEntity { + return artifactToSelectorEntity(m.t, m.name, m.class, artifact) +} + +func newMockProvider(t *testing.T, name, class string) *fullProvider { + t.Helper() + + return &fullProvider{ + name: name, + class: class, + t: t, + } +} + +type repoOnlyProvider struct { + name string + class string + t *testing.T +} + +func newRepoOnlyProvider(t *testing.T, name, class string) *repoOnlyProvider { + t.Helper() + + return &repoOnlyProvider{ + name: name, + class: class, + t: t, + } +} + +func (_ *repoOnlyProvider) CanImplement(_ minderv1.ProviderType) bool { + return true +} + +func (m *repoOnlyProvider) RepoToSelectorEntity(_ context.Context, repo *minderv1.Repository) *internalpb.SelectorEntity { + return repoToSelectorEntity(m.t, m.name, m.class, repo) +} + +func TestEntityToSelectorEntity(t *testing.T) { + t.Parallel() + + scenarios := []struct { + name string + provider provifv1.Provider + entityType minderv1.Entity + entity proto.Message + success bool + }{ + { + name: "Repository", + provider: newMockProvider(t, "github", "github"), + entityType: minderv1.Entity_ENTITY_REPOSITORIES, + entity: &minderv1.Repository{ + Owner: "testorg", + Name: "testrepo", + IsFork: true, + IsPrivate: true, + }, + success: true, + }, + { + name: "Artifact", + provider: newMockProvider(t, "github", "github"), + entityType: minderv1.Entity_ENTITY_ARTIFACTS, + entity: &minderv1.Artifact{ + Owner: "testorg", + Name: "testartifact", + Type: "container", + }, + success: true, + }, + { + name: "Repository with RepoOnlyProvider", + provider: newRepoOnlyProvider(t, "github", "github"), + entityType: minderv1.Entity_ENTITY_REPOSITORIES, + entity: &minderv1.Repository{ + Owner: "testorg", + Name: "testrepo", + IsFork: true, + IsPrivate: true, + }, + success: true, + }, + { + name: "Artifact with RepoOnlyProvider", + provider: newRepoOnlyProvider(t, "github", "github"), + entityType: minderv1.Entity_ENTITY_ARTIFACTS, + entity: &minderv1.Artifact{ + Owner: "testorg", + Name: "testartifact", + Type: "container", + }, + success: false, + }, + } + + for _, scenario := range scenarios { + scenario := scenario + + t.Run(scenario.name, func(t *testing.T) { + t.Parallel() + + selEnt := EntityToSelectorEntity( + context.Background(), + scenario.provider, + scenario.entityType, + scenario.entity, + ) + + if scenario.success { + require.NotNil(t, selEnt) + require.Equal(t, scenario.entityType, selEnt.GetEntityType()) + } else { + require.Nil(t, selEnt) + } + }) + } +}