diff --git a/internal/engine/selectors/mock/selectors.go b/internal/engine/selectors/mock/selectors.go new file mode 100644 index 0000000000..70a3f82a1c --- /dev/null +++ b/internal/engine/selectors/mock/selectors.go @@ -0,0 +1,95 @@ +// 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) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Select", arg0) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Select indicates an expected call of Select. +func (mr *MockSelectionMockRecorder) Select(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*MockSelection)(nil).Select), arg0) +} diff --git a/internal/engine/selectors/selectors.go b/internal/engine/selectors/selectors.go index 568fe754a7..70c25c0409 100644 --- a/internal/engine/selectors/selectors.go +++ b/internal/engine/selectors/selectors.go @@ -24,9 +24,7 @@ import ( "github.com/google/cel-go/cel" "github.com/google/cel-go/checker/decls" - "google.golang.org/protobuf/proto" - "github.com/stacklok/minder/internal/engine/entities" internalpb "github.com/stacklok/minder/internal/proto" minderv1 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1" ) @@ -42,7 +40,7 @@ func genericEnvFactory() (*cel.Env, error) { return newEnvForEntity( "entity", &internalpb.SelectorEntity{}, - "minder.v1.SelectorEntity") + "internal.SelectorEntity") } // repoEnvFactory is a factory for creating a CEL environment @@ -51,7 +49,7 @@ func repoEnvFactory() (*cel.Env, error) { return newEnvForEntity( "repository", &internalpb.SelectorRepository{}, - "minder.v1.SelectorRepository") + "internal.SelectorRepository") } // artifactEnvFactory is a factory for creating a CEL environment @@ -60,7 +58,7 @@ func artifactEnvFactory() (*cel.Env, error) { return newEnvForEntity( "artifact", &internalpb.SelectorArtifact{}, - "minder.v1.SelectorArtifact") + "internal.SelectorArtifact") } // newEnvForEntity creates a new CEL environment for an entity. All environments are allowed to @@ -201,94 +199,9 @@ func (e *Env) envForEntity(entity minderv1.Entity) (*cel.Env, error) { return cache.env, cache.err } -// entityInfoConverter is an interface for converting an entity from an EntityInfoWrapper to a SelectorEntity -type entityInfoConverter interface { - toSelectorEntity(entity proto.Message) *internalpb.SelectorEntity -} - -type repositoryInfoConverter struct{} - -func (_ *repositoryInfoConverter) toSelectorEntity(entity proto.Message) *internalpb.SelectorEntity { - r, ok := entity.(*minderv1.Repository) - if !ok { - return nil - } - - return &internalpb.SelectorEntity{ - EntityType: minderv1.Entity_ENTITY_REPOSITORIES, - Name: fmt.Sprintf("%s/%s", r.GetOwner(), r.GetName()), - Entity: &internalpb.SelectorEntity_Repository{ - Repository: &internalpb.SelectorRepository{ - Name: fmt.Sprintf("%s/%s", r.GetOwner(), r.GetName()), - IsFork: proto.Bool(r.GetIsFork()), - IsPrivate: proto.Bool(r.GetIsPrivate()), - }, - }, - } -} - -type artifactInfoConverter struct{} - -func (_ *artifactInfoConverter) toSelectorEntity(entity proto.Message) *internalpb.SelectorEntity { - a, ok := entity.(*minderv1.Artifact) - if !ok { - return nil - } - - return &internalpb.SelectorEntity{ - EntityType: minderv1.Entity_ENTITY_ARTIFACTS, - Name: fmt.Sprintf("%s/%s", a.GetOwner(), a.GetName()), - Entity: &internalpb.SelectorEntity_Artifact{ - Artifact: &internalpb.SelectorArtifact{ - Name: fmt.Sprintf("%s/%s", a.GetOwner(), a.GetName()), - Type: a.GetType(), - }, - }, - } -} - -// 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() *converterFactory { - return &converterFactory{ - converters: map[minderv1.Entity]entityInfoConverter{ - minderv1.Entity_ENTITY_REPOSITORIES: &repositoryInfoConverter{}, - minderv1.Entity_ENTITY_ARTIFACTS: &artifactInfoConverter{}, - }, - } -} - -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 -} - -func newSelectorEntity(eiw *entities.EntityInfoWrapper) *internalpb.SelectorEntity { - if eiw == nil { - return nil - } - - convertFactory := newConverterFactory() - conv, err := convertFactory.getConverter(eiw.Type) - if err != nil { - return nil - } - - return conv.toSelectorEntity(eiw.Entity) -} - // Selection is an interface for selecting entities based on a profile type Selection interface { Select(*internalpb.SelectorEntity) (bool, error) - SelectEiw(*entities.EntityInfoWrapper) (bool, error) } // EntitySelection is a struct that holds the compiled CEL expressions for a given entity type @@ -326,17 +239,6 @@ func (s *EntitySelection) Select(se *internalpb.SelectorEntity) (bool, error) { return true, nil } -// SelectEiw selects an entity based on an EntityInfoWrapper. It is a convenience method -// around Select that creates a SelectorEntity from the EntityInfoWrapper -func (s *EntitySelection) SelectEiw(eiw *entities.EntityInfoWrapper) (bool, error) { - se := newSelectorEntity(eiw) - if se == nil { - return false, fmt.Errorf("failed to create SelectorEntity from EntityInfoWrapper") - } - - return s.Select(se) -} - func inputAsMap(se *internalpb.SelectorEntity) (map[string]any, error) { var value any diff --git a/internal/engine/selectors/selectors_test.go b/internal/engine/selectors/selectors_test.go index cd83194393..e883b7455d 100644 --- a/internal/engine/selectors/selectors_test.go +++ b/internal/engine/selectors/selectors_test.go @@ -16,12 +16,11 @@ package selectors import ( - internalpb "github.com/stacklok/minder/internal/proto" "testing" "github.com/stretchr/testify/require" - "github.com/stacklok/minder/internal/engine/entities" + internalpb "github.com/stacklok/minder/internal/proto" minderv1 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1" ) @@ -301,118 +300,3 @@ func TestSelectSelectorEntity(t *testing.T) { }) } } - -func TestSelectEntityInfoWrapper(t *testing.T) { - t.Parallel() - - scenarios := []struct { - name string - eiwConstructor func() *entities.EntityInfoWrapper - exprs []*minderv1.Profile_Selector - selected bool - expectedErr string - }{ - { - name: "Simple true repository expression", - eiwConstructor: func() *entities.EntityInfoWrapper { - eiw := entities.NewEntityInfoWrapper() - eiw.WithRepository(&minderv1.Repository{ - Owner: "stacklok", - Name: "minder", - IsPrivate: false, - IsFork: false, - }) - return eiw - }, - exprs: []*minderv1.Profile_Selector{ - { - Entity: minderv1.RepositoryEntity.String(), - Selector: "entity.name == 'stacklok/minder'", - }, - }, - selected: true, - }, - { - name: "Simple true artifact expression", - eiwConstructor: func() *entities.EntityInfoWrapper { - eiw := entities.NewEntityInfoWrapper() - eiw.WithArtifact(&minderv1.Artifact{ - Owner: "stacklok", - Name: "minder", - Type: "container", - }) - return eiw - }, - exprs: []*minderv1.Profile_Selector{ - { - Entity: minderv1.ArtifactEntity.String(), - Selector: "artifact.type == 'container'", - }, - }, - selected: true, - }, - { - name: "Simple false artifact expression", - eiwConstructor: func() *entities.EntityInfoWrapper { - eiw := entities.NewEntityInfoWrapper() - eiw.WithArtifact(&minderv1.Artifact{ - Owner: "stacklok", - Name: "minder", - Type: "container", - }) - return eiw - }, - exprs: []*minderv1.Profile_Selector{ - { - Entity: minderv1.ArtifactEntity.String(), - Selector: "artifact.type != 'container'", - }, - }, - selected: false, - }, - { - name: "Simple false repository expression", - eiwConstructor: func() *entities.EntityInfoWrapper { - eiw := entities.NewEntityInfoWrapper() - eiw.WithRepository(&minderv1.Repository{ - Owner: "stacklok", - Name: "minder", - IsPrivate: false, - IsFork: false, - }) - return eiw - }, - exprs: []*minderv1.Profile_Selector{ - { - Entity: minderv1.RepositoryEntity.String(), - Selector: "entity.name != 'stacklok/minder'", - }, - }, - selected: false, - }, - } - for _, scenario := range scenarios { - t.Run(scenario.name, func(t *testing.T) { - t.Parallel() - - env, err := NewEnv() - require.NoError(t, err) - - eiw := scenario.eiwConstructor() - - sels, err := env.NewSelectionFromProfile(eiw.Type, scenario.exprs) - if scenario.expectedErr != "" { - require.Error(t, err) - require.Contains(t, err.Error(), scenario.expectedErr) - return - } - - require.NoError(t, err) - require.NotNil(t, sels) - - selected, err := sels.SelectEiw(eiw) - require.NoError(t, err) - require.Equal(t, scenario.selected, selected) - }) - } -} 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) + } + }) + } +}