From a4c9b01323eed66dde72bdd3a81aa9d3bb7661e2 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Thu, 4 Apr 2024 15:24:14 +0200 Subject: [PATCH] Support empty profiles by utilizing views (#2936) * Don't fail validation of an empty profile * Add a view over profiles and entity_profiles * Join over view, not the entity_profiles table * Remove unused query --- .../000046_entity_profiles_views.down.sql | 19 +++ .../000046_entity_profiles_views.up.sql | 21 +++ database/mock/store.go | 15 -- database/query/profiles.sql | 10 +- .../controlplane/handlers_profile_test.go | 10 +- internal/db/models.go | 11 ++ internal/db/profiles.sql.go | 152 +++++------------- internal/db/querier.go | 1 - internal/db/store.go | 22 ++- internal/engine/executor_test.go | 13 +- internal/engine/profile.go | 16 +- internal/profiles/validator_test.go | 5 - pkg/api/protobuf/go/minder/v1/validators.go | 10 -- 13 files changed, 143 insertions(+), 162 deletions(-) create mode 100644 database/migrations/000046_entity_profiles_views.down.sql create mode 100644 database/migrations/000046_entity_profiles_views.up.sql diff --git a/database/migrations/000046_entity_profiles_views.down.sql b/database/migrations/000046_entity_profiles_views.down.sql new file mode 100644 index 0000000000..ae3f4f53b8 --- /dev/null +++ b/database/migrations/000046_entity_profiles_views.down.sql @@ -0,0 +1,19 @@ +-- 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. + +BEGIN; + +DROP VIEW IF EXISTS profiles_with_entity_profiles; + +COMMIT; \ No newline at end of file diff --git a/database/migrations/000046_entity_profiles_views.up.sql b/database/migrations/000046_entity_profiles_views.up.sql new file mode 100644 index 0000000000..81ca57f5ea --- /dev/null +++ b/database/migrations/000046_entity_profiles_views.up.sql @@ -0,0 +1,21 @@ +-- 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. + +BEGIN; + +CREATE VIEW profiles_with_entity_profiles AS( + SELECT entity_profiles.*, profiles.id as profid FROM profiles LEFT JOIN entity_profiles ON profiles.id = entity_profiles.profile_id +); + +COMMIT; \ No newline at end of file diff --git a/database/mock/store.go b/database/mock/store.go index 5bcea097fe..6c39c2f4fd 100644 --- a/database/mock/store.go +++ b/database/mock/store.go @@ -702,21 +702,6 @@ func (mr *MockStoreMockRecorder) GetChildrenProjects(arg0, arg1 any) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChildrenProjects", reflect.TypeOf((*MockStore)(nil).GetChildrenProjects), arg0, arg1) } -// GetEntityProfileByProjectAndName mocks base method. -func (m *MockStore) GetEntityProfileByProjectAndName(arg0 context.Context, arg1 db.GetEntityProfileByProjectAndNameParams) ([]db.GetEntityProfileByProjectAndNameRow, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetEntityProfileByProjectAndName", arg0, arg1) - ret0, _ := ret[0].([]db.GetEntityProfileByProjectAndNameRow) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetEntityProfileByProjectAndName indicates an expected call of GetEntityProfileByProjectAndName. -func (mr *MockStoreMockRecorder) GetEntityProfileByProjectAndName(arg0, arg1 any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEntityProfileByProjectAndName", reflect.TypeOf((*MockStore)(nil).GetEntityProfileByProjectAndName), arg0, arg1) -} - // GetFeatureInProject mocks base method. func (m *MockStore) GetFeatureInProject(arg0 context.Context, arg1 db.GetFeatureInProjectParams) (json.RawMessage, error) { m.ctrl.T.Helper() diff --git a/database/query/profiles.sql b/database/query/profiles.sql index 6081cbba54..7fcc2bfa1a 100644 --- a/database/query/profiles.sql +++ b/database/query/profiles.sql @@ -40,7 +40,7 @@ DELETE FROM entity_profiles WHERE profile_id = $1 AND entity = $2; SELECT * FROM entity_profiles WHERE profile_id = $1 AND entity = $2; -- name: GetProfileByProjectAndID :many -SELECT * FROM profiles JOIN entity_profiles ON profiles.id = entity_profiles.profile_id +SELECT * FROM profiles JOIN profiles_with_entity_profiles ON profiles.id = profiles_with_entity_profiles.profid WHERE profiles.project_id = $1 AND profiles.id = $2; -- name: GetProfileByID :one @@ -52,16 +52,12 @@ SELECT * FROM profiles WHERE id = $1 AND project_id = $2 FOR UPDATE; -- name: GetProfileByNameAndLock :one SELECT * FROM profiles WHERE lower(name) = lower(sqlc.arg(name)) AND project_id = $1 FOR UPDATE; --- name: GetEntityProfileByProjectAndName :many -SELECT * FROM profiles JOIN entity_profiles ON profiles.id = entity_profiles.profile_id -WHERE profiles.project_id = $1 AND lower(profiles.name) = lower(sqlc.arg(name)); - -- name: ListProfilesByProjectID :many -SELECT sqlc.embed(profiles), sqlc.embed(entity_profiles) FROM profiles JOIN entity_profiles ON profiles.id = entity_profiles.profile_id +SELECT sqlc.embed(profiles), sqlc.embed(profiles_with_entity_profiles) FROM profiles JOIN profiles_with_entity_profiles ON profiles.id = profiles_with_entity_profiles.profid WHERE profiles.project_id = $1; -- name: ListProfilesByProjectIDAndLabel :many -SELECT sqlc.embed(profiles), sqlc.embed(entity_profiles) FROM profiles JOIN entity_profiles ON profiles.id = entity_profiles.profile_id +SELECT sqlc.embed(profiles), sqlc.embed(profiles_with_entity_profiles) FROM profiles JOIN profiles_with_entity_profiles ON profiles.id = profiles_with_entity_profiles.profid WHERE profiles.project_id = $1 AND ( -- the most common case first, if the include_labels is empty, we list profiles with no labels diff --git a/internal/controlplane/handlers_profile_test.go b/internal/controlplane/handlers_profile_test.go index 1e372a49e0..62ede18ff3 100644 --- a/internal/controlplane/handlers_profile_test.go +++ b/internal/controlplane/handlers_profile_test.go @@ -262,10 +262,16 @@ func TestCreateProfile(t *testing.T) { name: "Create profile with no rules", profile: &minderv1.CreateProfileRequest{ Profile: &minderv1.Profile{ - Name: "test", + Name: "test_norules", + }, + }, + result: &minderv1.CreateProfileResponse{ + Profile: &minderv1.Profile{ + Name: "test_norules", + Alert: proto.String("on"), + Remediate: proto.String("off"), }, }, - wantErr: `Couldn't create profile: validation failed: profile must have at least one rule`, }, { name: "Create profile with valid name and rules", diff --git a/internal/db/models.go b/internal/db/models.go index 58b6b4d7f3..a0c68de903 100644 --- a/internal/db/models.go +++ b/internal/db/models.go @@ -12,6 +12,7 @@ import ( "time" "github.com/google/uuid" + "github.com/sqlc-dev/pqtype" ) type ActionType string @@ -505,6 +506,16 @@ type ProfileStatus struct { LastUpdated time.Time `json:"last_updated"` } +type ProfilesWithEntityProfile struct { + ID uuid.NullUUID `json:"id"` + Entity NullEntities `json:"entity"` + ProfileID uuid.NullUUID `json:"profile_id"` + ContextualRules pqtype.NullRawMessage `json:"contextual_rules"` + CreatedAt sql.NullTime `json:"created_at"` + UpdatedAt sql.NullTime `json:"updated_at"` + Profid uuid.UUID `json:"profid"` +} + type Project struct { ID uuid.UUID `json:"id"` Name string `json:"name"` diff --git a/internal/db/profiles.sql.go b/internal/db/profiles.sql.go index 6f81a3ce9a..e5a42de916 100644 --- a/internal/db/profiles.sql.go +++ b/internal/db/profiles.sql.go @@ -13,6 +13,7 @@ import ( "github.com/google/uuid" "github.com/lib/pq" + "github.com/sqlc-dev/pqtype" ) const countProfilesByEntityType = `-- name: CountProfilesByEntityType :many @@ -181,79 +182,6 @@ func (q *Queries) DeleteRuleInstantiation(ctx context.Context, arg DeleteRuleIns return err } -const getEntityProfileByProjectAndName = `-- name: GetEntityProfileByProjectAndName :many -SELECT profiles.id, name, provider, project_id, remediate, alert, profiles.created_at, profiles.updated_at, provider_id, subscription_id, display_name, labels, entity_profiles.id, entity, profile_id, contextual_rules, entity_profiles.created_at, entity_profiles.updated_at FROM profiles JOIN entity_profiles ON profiles.id = entity_profiles.profile_id -WHERE profiles.project_id = $1 AND lower(profiles.name) = lower($2) -` - -type GetEntityProfileByProjectAndNameParams struct { - ProjectID uuid.UUID `json:"project_id"` - Name string `json:"name"` -} - -type GetEntityProfileByProjectAndNameRow struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - Provider sql.NullString `json:"provider"` - ProjectID uuid.UUID `json:"project_id"` - Remediate NullActionType `json:"remediate"` - Alert NullActionType `json:"alert"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - ProviderID uuid.NullUUID `json:"provider_id"` - SubscriptionID uuid.NullUUID `json:"subscription_id"` - DisplayName string `json:"display_name"` - Labels []string `json:"labels"` - ID_2 uuid.UUID `json:"id_2"` - Entity Entities `json:"entity"` - ProfileID uuid.UUID `json:"profile_id"` - ContextualRules json.RawMessage `json:"contextual_rules"` - CreatedAt_2 time.Time `json:"created_at_2"` - UpdatedAt_2 time.Time `json:"updated_at_2"` -} - -func (q *Queries) GetEntityProfileByProjectAndName(ctx context.Context, arg GetEntityProfileByProjectAndNameParams) ([]GetEntityProfileByProjectAndNameRow, error) { - rows, err := q.db.QueryContext(ctx, getEntityProfileByProjectAndName, arg.ProjectID, arg.Name) - if err != nil { - return nil, err - } - defer rows.Close() - items := []GetEntityProfileByProjectAndNameRow{} - for rows.Next() { - var i GetEntityProfileByProjectAndNameRow - if err := rows.Scan( - &i.ID, - &i.Name, - &i.Provider, - &i.ProjectID, - &i.Remediate, - &i.Alert, - &i.CreatedAt, - &i.UpdatedAt, - &i.ProviderID, - &i.SubscriptionID, - &i.DisplayName, - pq.Array(&i.Labels), - &i.ID_2, - &i.Entity, - &i.ProfileID, - &i.ContextualRules, - &i.CreatedAt_2, - &i.UpdatedAt_2, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - const getProfileByID = `-- name: GetProfileByID :one SELECT id, name, provider, project_id, remediate, alert, created_at, updated_at, provider_id, subscription_id, display_name, labels FROM profiles WHERE id = $1 AND project_id = $2 ` @@ -342,7 +270,7 @@ func (q *Queries) GetProfileByNameAndLock(ctx context.Context, arg GetProfileByN } const getProfileByProjectAndID = `-- name: GetProfileByProjectAndID :many -SELECT profiles.id, name, provider, project_id, remediate, alert, profiles.created_at, profiles.updated_at, provider_id, subscription_id, display_name, labels, entity_profiles.id, entity, profile_id, contextual_rules, entity_profiles.created_at, entity_profiles.updated_at FROM profiles JOIN entity_profiles ON profiles.id = entity_profiles.profile_id +SELECT profiles.id, name, provider, project_id, remediate, alert, profiles.created_at, profiles.updated_at, provider_id, subscription_id, display_name, labels, profiles_with_entity_profiles.id, entity, profile_id, contextual_rules, profiles_with_entity_profiles.created_at, profiles_with_entity_profiles.updated_at, profid FROM profiles JOIN profiles_with_entity_profiles ON profiles.id = profiles_with_entity_profiles.profid WHERE profiles.project_id = $1 AND profiles.id = $2 ` @@ -352,24 +280,25 @@ type GetProfileByProjectAndIDParams struct { } type GetProfileByProjectAndIDRow struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - Provider sql.NullString `json:"provider"` - ProjectID uuid.UUID `json:"project_id"` - Remediate NullActionType `json:"remediate"` - Alert NullActionType `json:"alert"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - ProviderID uuid.NullUUID `json:"provider_id"` - SubscriptionID uuid.NullUUID `json:"subscription_id"` - DisplayName string `json:"display_name"` - Labels []string `json:"labels"` - ID_2 uuid.UUID `json:"id_2"` - Entity Entities `json:"entity"` - ProfileID uuid.UUID `json:"profile_id"` - ContextualRules json.RawMessage `json:"contextual_rules"` - CreatedAt_2 time.Time `json:"created_at_2"` - UpdatedAt_2 time.Time `json:"updated_at_2"` + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Provider sql.NullString `json:"provider"` + ProjectID uuid.UUID `json:"project_id"` + Remediate NullActionType `json:"remediate"` + Alert NullActionType `json:"alert"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + ProviderID uuid.NullUUID `json:"provider_id"` + SubscriptionID uuid.NullUUID `json:"subscription_id"` + DisplayName string `json:"display_name"` + Labels []string `json:"labels"` + ID_2 uuid.NullUUID `json:"id_2"` + Entity NullEntities `json:"entity"` + ProfileID uuid.NullUUID `json:"profile_id"` + ContextualRules pqtype.NullRawMessage `json:"contextual_rules"` + CreatedAt_2 sql.NullTime `json:"created_at_2"` + UpdatedAt_2 sql.NullTime `json:"updated_at_2"` + Profid uuid.UUID `json:"profid"` } func (q *Queries) GetProfileByProjectAndID(ctx context.Context, arg GetProfileByProjectAndIDParams) ([]GetProfileByProjectAndIDRow, error) { @@ -400,6 +329,7 @@ func (q *Queries) GetProfileByProjectAndID(ctx context.Context, arg GetProfileBy &i.ContextualRules, &i.CreatedAt_2, &i.UpdatedAt_2, + &i.Profid, ); err != nil { return nil, err } @@ -438,13 +368,13 @@ func (q *Queries) GetProfileForEntity(ctx context.Context, arg GetProfileForEnti } const listProfilesByProjectID = `-- name: ListProfilesByProjectID :many -SELECT profiles.id, profiles.name, profiles.provider, profiles.project_id, profiles.remediate, profiles.alert, profiles.created_at, profiles.updated_at, profiles.provider_id, profiles.subscription_id, profiles.display_name, profiles.labels, entity_profiles.id, entity_profiles.entity, entity_profiles.profile_id, entity_profiles.contextual_rules, entity_profiles.created_at, entity_profiles.updated_at FROM profiles JOIN entity_profiles ON profiles.id = entity_profiles.profile_id +SELECT profiles.id, profiles.name, profiles.provider, profiles.project_id, profiles.remediate, profiles.alert, profiles.created_at, profiles.updated_at, profiles.provider_id, profiles.subscription_id, profiles.display_name, profiles.labels, profiles_with_entity_profiles.id, profiles_with_entity_profiles.entity, profiles_with_entity_profiles.profile_id, profiles_with_entity_profiles.contextual_rules, profiles_with_entity_profiles.created_at, profiles_with_entity_profiles.updated_at, profiles_with_entity_profiles.profid FROM profiles JOIN profiles_with_entity_profiles ON profiles.id = profiles_with_entity_profiles.profid WHERE profiles.project_id = $1 ` type ListProfilesByProjectIDRow struct { - Profile Profile `json:"profile"` - EntityProfile EntityProfile `json:"entity_profile"` + Profile Profile `json:"profile"` + ProfilesWithEntityProfile ProfilesWithEntityProfile `json:"profiles_with_entity_profile"` } func (q *Queries) ListProfilesByProjectID(ctx context.Context, projectID uuid.UUID) ([]ListProfilesByProjectIDRow, error) { @@ -469,12 +399,13 @@ func (q *Queries) ListProfilesByProjectID(ctx context.Context, projectID uuid.UU &i.Profile.SubscriptionID, &i.Profile.DisplayName, pq.Array(&i.Profile.Labels), - &i.EntityProfile.ID, - &i.EntityProfile.Entity, - &i.EntityProfile.ProfileID, - &i.EntityProfile.ContextualRules, - &i.EntityProfile.CreatedAt, - &i.EntityProfile.UpdatedAt, + &i.ProfilesWithEntityProfile.ID, + &i.ProfilesWithEntityProfile.Entity, + &i.ProfilesWithEntityProfile.ProfileID, + &i.ProfilesWithEntityProfile.ContextualRules, + &i.ProfilesWithEntityProfile.CreatedAt, + &i.ProfilesWithEntityProfile.UpdatedAt, + &i.ProfilesWithEntityProfile.Profid, ); err != nil { return nil, err } @@ -490,7 +421,7 @@ func (q *Queries) ListProfilesByProjectID(ctx context.Context, projectID uuid.UU } const listProfilesByProjectIDAndLabel = `-- name: ListProfilesByProjectIDAndLabel :many -SELECT profiles.id, profiles.name, profiles.provider, profiles.project_id, profiles.remediate, profiles.alert, profiles.created_at, profiles.updated_at, profiles.provider_id, profiles.subscription_id, profiles.display_name, profiles.labels, entity_profiles.id, entity_profiles.entity, entity_profiles.profile_id, entity_profiles.contextual_rules, entity_profiles.created_at, entity_profiles.updated_at FROM profiles JOIN entity_profiles ON profiles.id = entity_profiles.profile_id +SELECT profiles.id, profiles.name, profiles.provider, profiles.project_id, profiles.remediate, profiles.alert, profiles.created_at, profiles.updated_at, profiles.provider_id, profiles.subscription_id, profiles.display_name, profiles.labels, profiles_with_entity_profiles.id, profiles_with_entity_profiles.entity, profiles_with_entity_profiles.profile_id, profiles_with_entity_profiles.contextual_rules, profiles_with_entity_profiles.created_at, profiles_with_entity_profiles.updated_at, profiles_with_entity_profiles.profid FROM profiles JOIN profiles_with_entity_profiles ON profiles.id = profiles_with_entity_profiles.profid WHERE profiles.project_id = $1 AND ( -- the most common case first, if the include_labels is empty, we list profiles with no labels @@ -515,8 +446,8 @@ type ListProfilesByProjectIDAndLabelParams struct { } type ListProfilesByProjectIDAndLabelRow struct { - Profile Profile `json:"profile"` - EntityProfile EntityProfile `json:"entity_profile"` + Profile Profile `json:"profile"` + ProfilesWithEntityProfile ProfilesWithEntityProfile `json:"profiles_with_entity_profile"` } func (q *Queries) ListProfilesByProjectIDAndLabel(ctx context.Context, arg ListProfilesByProjectIDAndLabelParams) ([]ListProfilesByProjectIDAndLabelRow, error) { @@ -541,12 +472,13 @@ func (q *Queries) ListProfilesByProjectIDAndLabel(ctx context.Context, arg ListP &i.Profile.SubscriptionID, &i.Profile.DisplayName, pq.Array(&i.Profile.Labels), - &i.EntityProfile.ID, - &i.EntityProfile.Entity, - &i.EntityProfile.ProfileID, - &i.EntityProfile.ContextualRules, - &i.EntityProfile.CreatedAt, - &i.EntityProfile.UpdatedAt, + &i.ProfilesWithEntityProfile.ID, + &i.ProfilesWithEntityProfile.Entity, + &i.ProfilesWithEntityProfile.ProfileID, + &i.ProfilesWithEntityProfile.ContextualRules, + &i.ProfilesWithEntityProfile.CreatedAt, + &i.ProfilesWithEntityProfile.UpdatedAt, + &i.ProfilesWithEntityProfile.Profid, ); err != nil { return nil, err } diff --git a/internal/db/querier.go b/internal/db/querier.go index 747d00ba0d..f793721f29 100644 --- a/internal/db/querier.go +++ b/internal/db/querier.go @@ -60,7 +60,6 @@ type Querier interface { GetArtifactByName(ctx context.Context, arg GetArtifactByNameParams) (GetArtifactByNameRow, error) GetBundle(ctx context.Context, arg GetBundleParams) (Bundle, error) GetChildrenProjects(ctx context.Context, id uuid.UUID) ([]GetChildrenProjectsRow, error) - GetEntityProfileByProjectAndName(ctx context.Context, arg GetEntityProfileByProjectAndNameParams) ([]GetEntityProfileByProjectAndNameRow, error) // GetFeatureInProject verifies if a feature is available for a specific project. // It returns the settings for the feature if it is available. GetFeatureInProject(ctx context.Context, arg GetFeatureInProjectParams) (json.RawMessage, error) diff --git a/internal/db/store.go b/internal/db/store.go index 15296ebc10..9ee3aeed34 100644 --- a/internal/db/store.go +++ b/internal/db/store.go @@ -22,6 +22,7 @@ import ( "strings" "github.com/google/uuid" + "github.com/sqlc-dev/pqtype" ) // ExtendQuerier extends the Querier interface with custom queries @@ -136,7 +137,8 @@ func WithTransaction[T any](store Store, fn func(querier ExtendQuerier) (T, erro // ProfileRow is an interface row in the profiles table type ProfileRow interface { GetProfile() Profile - GetEntityProfile() EntityProfile + GetEntityProfile() NullEntities + GetContextualRules() pqtype.NullRawMessage } // GetProfile returns the profile @@ -145,8 +147,13 @@ func (r ListProfilesByProjectIDAndLabelRow) GetProfile() Profile { } // GetEntityProfile returns the entity profile -func (r ListProfilesByProjectIDAndLabelRow) GetEntityProfile() EntityProfile { - return r.EntityProfile +func (r ListProfilesByProjectIDAndLabelRow) GetEntityProfile() NullEntities { + return r.ProfilesWithEntityProfile.Entity +} + +// GetContextualRules returns the contextual rules +func (r ListProfilesByProjectIDAndLabelRow) GetContextualRules() pqtype.NullRawMessage { + return r.ProfilesWithEntityProfile.ContextualRules } // GetProfile returns the profile @@ -155,8 +162,13 @@ func (r ListProfilesByProjectIDRow) GetProfile() Profile { } // GetEntityProfile returns the entity profile -func (r ListProfilesByProjectIDRow) GetEntityProfile() EntityProfile { - return r.EntityProfile +func (r ListProfilesByProjectIDRow) GetEntityProfile() NullEntities { + return r.ProfilesWithEntityProfile.Entity +} + +// GetContextualRules returns the contextual rules +func (r ListProfilesByProjectIDRow) GetContextualRules() pqtype.NullRawMessage { + return r.ProfilesWithEntityProfile.ContextualRules } // LabelsFromFilter parses the filter string and populates the IncludeLabels and ExcludeLabels fields diff --git a/internal/engine/executor_test.go b/internal/engine/executor_test.go index a31db32a41..8b1b17b1dd 100644 --- a/internal/engine/executor_test.go +++ b/internal/engine/executor_test.go @@ -24,6 +24,7 @@ import ( "time" "github.com/google/uuid" + "github.com/sqlc-dev/pqtype" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "golang.org/x/oauth2" @@ -154,9 +155,15 @@ func TestExecutor_handleEntityEvent(t *testing.T) { CreatedAt: time.Now(), UpdatedAt: time.Now(), }, - EntityProfile: db.EntityProfile{ - Entity: db.EntitiesRepository, - ContextualRules: json.RawMessage(marshalledCRS), + ProfilesWithEntityProfile: db.ProfilesWithEntityProfile{ + Entity: db.NullEntities{ + Entities: db.EntitiesRepository, + Valid: true, + }, + ContextualRules: pqtype.NullRawMessage{ + RawMessage: json.RawMessage(marshalledCRS), + Valid: true, + }, }, }, }, nil) diff --git a/internal/engine/profile.go b/internal/engine/profile.go index 23af49d48a..9e61faf482 100644 --- a/internal/engine/profile.go +++ b/internal/engine/profile.go @@ -24,6 +24,7 @@ import ( "path/filepath" "github.com/rs/zerolog/log" + "github.com/sqlc-dev/pqtype" "google.golang.org/protobuf/proto" "github.com/stacklok/minder/internal/db" @@ -214,8 +215,8 @@ func MergeDatabaseListIntoProfiles[T db.ProfileRow](ppl []T) map[string]*pb.Prof } } if pm := rowInfoToProfileMap( - profiles[p.GetProfile().Name], p.GetEntityProfile().Entity, - p.GetEntityProfile().ContextualRules); pm != nil { + profiles[p.GetProfile().Name], p.GetEntityProfile(), + p.GetContextualRules()); pm != nil { profiles[p.GetProfile().Name] = pm } } @@ -281,9 +282,16 @@ func MergeDatabaseGetIntoProfiles(ppl []db.GetProfileByProjectAndIDRow) map[stri // and thus the logic is targetted to that. func rowInfoToProfileMap( profile *pb.Profile, - entity db.Entities, - contextualRules json.RawMessage, + maybeEntity db.NullEntities, + maybeContextualRules pqtype.NullRawMessage, ) *pb.Profile { + if !maybeEntity.Valid || !maybeContextualRules.Valid { + // empty profile. Just return without filling in the rules + return profile + } + entity := maybeEntity.Entities + contextualRules := maybeContextualRules.RawMessage + if !entities.EntityTypeFromDB(entity).IsValid() { log.Printf("unknown entity found in database: %s", entity) return nil diff --git a/internal/profiles/validator_test.go b/internal/profiles/validator_test.go index cbc2a949bb..c8aecce7e1 100644 --- a/internal/profiles/validator_test.go +++ b/internal/profiles/validator_test.go @@ -55,11 +55,6 @@ func TestValidatorScenarios(t *testing.T) { Profile: makeProfile(), ExpectedError: "invalid profile", }, - { - Name: "Validator rejects profile with no rules defined", - Profile: makeProfile(withBasicProfileData), - ExpectedError: "profile must have at least one rule", - }, { Name: "Validator rejects profile with multiple unnamed rules of same type", Profile: makeProfile(withBasicProfileData, withRules(makeRule(withEmptyRuleName), makeRule(withEmptyRuleName))), diff --git a/pkg/api/protobuf/go/minder/v1/validators.go b/pkg/api/protobuf/go/minder/v1/validators.go index afe44e2917..8ebc4c2788 100644 --- a/pkg/api/protobuf/go/minder/v1/validators.go +++ b/pkg/api/protobuf/go/minder/v1/validators.go @@ -208,16 +208,6 @@ func (p *Profile) Validate() error { return fmt.Errorf("%w: %w", ErrValidationFailed, err) } - repoRuleCount := len(p.GetRepository()) - buildEnvRuleCount := len(p.GetBuildEnvironment()) - artifactRuleCount := len(p.GetArtifact()) - pullRequestRuleCount := len(p.GetPullRequest()) - totalRuleCount := repoRuleCount + buildEnvRuleCount + artifactRuleCount + pullRequestRuleCount - - if totalRuleCount == 0 { - return fmt.Errorf("%w: profile must have at least one rule", ErrValidationFailed) - } - // If the profile is nil or empty, we don't need to validate it for i, r := range p.GetRepository() { if err := validateRule(r); err != nil {