diff --git a/cmd/cli/app/profile/status/status_get.go b/cmd/cli/app/profile/status/status_get.go index d0b46a58a8..4b94090ce7 100644 --- a/cmd/cli/app/profile/status/status_get.go +++ b/cmd/cli/app/profile/status/status_get.go @@ -8,6 +8,7 @@ import ( "fmt" "os" + "github.com/google/uuid" "github.com/spf13/cobra" "github.com/spf13/viper" "google.golang.org/grpc" @@ -44,14 +45,24 @@ func getCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *grpc. return cli.MessageAndError(fmt.Sprintf("Output format %s not supported", format), fmt.Errorf("invalid argument")) } + entity := &minderv1.EntityTypedId{ + Type: minderv1.EntityFromString(entityType), + } + // If entityId is a UUID, fill the `id` field, otherwise fill the name field. + if _, err := uuid.Parse(entityId); err == nil { + entity.Id = entityId + } else { + entity.Name = entityId + } + if profileId != "" { - resp, err := getProfileStatusById(ctx, client, project, profileId, entityId, entityType) + resp, err := getProfileStatusById(ctx, client, project, profileId, entity) if err != nil { return cli.MessageAndError("Error getting profile status", err) } return formatAndDisplayOutput(cmd, format, resp, viper.GetBool("emoji")) } else if profileName != "" { - resp, err := getProfileStatusByName(ctx, client, project, profileName, entityId, entityType) + resp, err := getProfileStatusByName(ctx, client, project, profileName, entity) if err != nil { return cli.MessageAndError("Error getting profile status", err) } @@ -64,7 +75,8 @@ func getCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *grpc. func getProfileStatusById( ctx context.Context, client minderv1.ProfileServiceClient, - project, profileId, entityId, entityType string, + project, profileId string, + entity *minderv1.EntityTypedId, ) (*minderv1.GetProfileStatusByIdResponse, error) { if profileId == "" { return nil, cli.MessageAndError("Error getting profile status", fmt.Errorf("profile id required")) @@ -73,10 +85,7 @@ func getProfileStatusById( resp, err := client.GetProfileStatusById(ctx, &minderv1.GetProfileStatusByIdRequest{ Context: &minderv1.Context{Project: &project}, Id: profileId, - Entity: &minderv1.EntityTypedId{ - Id: entityId, - Type: minderv1.EntityFromString(entityType), - }, + Entity: entity, }) if err != nil { return nil, err @@ -91,7 +100,8 @@ func getProfileStatusById( func getProfileStatusByName( ctx context.Context, client minderv1.ProfileServiceClient, - project, profileName, entityId, entityType string, + project, profileName string, + entity *minderv1.EntityTypedId, ) (*minderv1.GetProfileStatusByNameResponse, error) { if profileName == "" { return nil, cli.MessageAndError("Error getting profile status", fmt.Errorf("profile name required")) @@ -100,10 +110,7 @@ func getProfileStatusByName( resp, err := client.GetProfileStatusByName(ctx, &minderv1.GetProfileStatusByNameRequest{ Context: &minderv1.Context{Project: &project}, Name: profileName, - Entity: &minderv1.EntityTypedId{ - Id: entityId, - Type: minderv1.EntityFromString(entityType), - }, + Entity: entity, }) if err != nil { return nil, err diff --git a/cmd/cli/app/repo/repo_reconcile.go b/cmd/cli/app/repo/repo_reconcile.go index a3353a945e..78d63c3229 100644 --- a/cmd/cli/app/repo/repo_reconcile.go +++ b/cmd/cli/app/repo/repo_reconcile.go @@ -33,29 +33,19 @@ func reconcileCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn // See https://github.com/spf13/cobra/issues/340#issuecomment-374617413 cmd.SilenceUsage = true - if id == "" { - repoClient := minderv1.NewRepositoryServiceClient(conn) - - repo, err := repoClient.GetRepositoryByName(ctx, &minderv1.GetRepositoryByNameRequest{ - Name: name, - Context: &minderv1.Context{ - Provider: &provider, - Project: &project, - }, - }) - if err != nil { - return cli.MessageAndError("Failed to get repository", err) - } - - id = repo.GetRepository().GetId() + entity := &minderv1.EntityTypedId{ + Type: minderv1.Entity_ENTITY_REPOSITORIES, + } + if id != "" { + entity.Id = id + } + if name != "" { + entity.Name = name } projectsClient := minderv1.NewProjectsServiceClient(conn) _, err := projectsClient.CreateEntityReconciliationTask(ctx, &minderv1.CreateEntityReconciliationTaskRequest{ - Entity: &minderv1.EntityTypedId{ - Id: id, - Type: minderv1.Entity_ENTITY_REPOSITORIES, - }, + Entity: entity, Context: &minderv1.Context{ Provider: &provider, Project: &project, diff --git a/database/query/profile_status.sql b/database/query/profile_status.sql index f0c4bfcd6e..a48a5450e3 100644 --- a/database/query/profile_status.sql +++ b/database/query/profile_status.sql @@ -88,6 +88,7 @@ SELECT rt.guidance as rule_type_guidance, rt.display_name as rule_type_display_name, ere.entity_instance_id as entity_id, + ei.name as entity_name, ei.project_id as project_id, rt.release_phase as rule_type_release_phase FROM latest_evaluation_statuses les @@ -99,8 +100,9 @@ FROM latest_evaluation_statuses les INNER JOIN rule_type rt ON rt.id = ri.rule_type_id INNER JOIN entity_instances ei ON ei.id = ere.entity_instance_id INNER JOIN providers prov ON prov.id = ei.provider_id -WHERE les.profile_id = $1 AND - (ere.entity_instance_id = sqlc.narg(entity_id)::UUID OR sqlc.narg(entity_id)::UUID IS NULL) +WHERE les.profile_id = $1 + AND (ere.entity_instance_id = sqlc.narg(entity_id)::UUID OR sqlc.narg(entity_id)::UUID IS NULL) + AND (ei.name = sqlc.narg(entity_name) OR sqlc.narg(entity_name) IS NULL) AND (rt.name = sqlc.narg(rule_type_name) OR sqlc.narg(rule_type_name) IS NULL) AND (lower(ri.name) = lower(sqlc.narg(rule_name)) OR sqlc.narg(rule_name) IS NULL) ; diff --git a/docs/docs/ref/proto.mdx b/docs/docs/ref/proto.mdx index 05e47a69e6..d79c545ad5 100644 --- a/docs/docs/ref/proto.mdx +++ b/docs/docs/ref/proto.mdx @@ -965,14 +965,17 @@ used for parsing resources in ruletypes EntityTypedId -EntiryTypeId is a message that carries an ID together with a type to uniquely identify an entity +EntityTypedId is a message that carries an ID together with a type to uniquely identify an entity such as (repo, 1), (artifact, 2), ... | Field | Type | Label | Description | | ----- | ---- | ----- | ----------- | -| type | Entity | | entity is the entity to get status for. Incompatible with `all` | +| type | Entity | | entity is the entity to get status for. Incompatible with `all` + +On input, at least one of id and name must be set. If both are set, they must both match. On output, both id and name will be set. | | id | string | | id is the ID of the entity to get status for. Incompatible with `all` | +| name | string | | name is the name of the entity. This name is unique within a given project, type, and provider, but may not be globally unique. | diff --git a/internal/controlplane/handlers_evalstatus.go b/internal/controlplane/handlers_evalstatus.go index 9c49eb05f4..d10423f162 100644 --- a/internal/controlplane/handlers_evalstatus.go +++ b/internal/controlplane/handlers_evalstatus.go @@ -666,11 +666,10 @@ func (s *Server) buildRuleEvaluationStatusFromDBEvaluation( func buildEntityFromEvaluation(efp *entmodels.EntityWithProperties) *minderv1.EntityTypedId { ent := &minderv1.EntityTypedId{ Type: efp.Entity.Type, + Id: efp.Entity.ID.String(), + Name: efp.Entity.Name, } - if ent.Type == minderv1.Entity_ENTITY_REPOSITORIES { - ent.Id = efp.Entity.ID.String() - } return ent } diff --git a/internal/controlplane/handlers_profile.go b/internal/controlplane/handlers_profile.go index 030e2c4c71..93765fe322 100644 --- a/internal/controlplane/handlers_profile.go +++ b/internal/controlplane/handlers_profile.go @@ -617,17 +617,6 @@ func (s *Server) GetProfileStatusById( }, nil } -func extractEntitySelector(entity *minderv1.EntityTypedId) *uuid.NullUUID { - if entity == nil { - return nil - } - var selector uuid.NullUUID - if err := selector.Scan(entity.GetId()); err != nil { - return nil - } - return &selector -} - func (s *Server) processProfileStatusByName( ctx context.Context, profileName string, @@ -638,21 +627,29 @@ func (s *Server) processProfileStatusByName( ) (*minderv1.GetProfileStatusByNameResponse, error) { var ruleEvaluationStatuses []*minderv1.RuleEvaluationStatus - selector, ruleType, ruleName, err := extractFiltersFromNameRequest(req) - if err != nil { + // Telemetry logging + entityCtx := engcontext.EntityFromContext(ctx) + logger.BusinessRecord(ctx).Project = entityCtx.Project.ID + logger.BusinessRecord(ctx).Profile = logger.Profile{Name: profileName, ID: profileID} + + if err := validateEntityType(req.GetEntity()); err != nil { return nil, err } + maybeEntityID, err := maybeNullUUID(req.GetEntity().GetId()) + if err != nil { + return nil, util.UserVisibleError(codes.InvalidArgument, "Unable to parse entity id: %q", req.GetEntity().GetId()) + } + maybeEntityName := maybeNullString(req.GetEntity().GetName()) + ruleType := maybeNullString(req.GetRuleType()) + ruleName := maybeNullString(req.GetRuleName()) - if selector != nil || req.GetAll() { - var entityID uuid.NullUUID - if selector != nil { - entityID = *selector - } + if req.GetAll() || maybeEntityID.Valid || maybeEntityName.Valid { dbRuleEvaluationStatuses, err := s.store.ListRuleEvaluationsByProfileId(ctx, db.ListRuleEvaluationsByProfileIdParams{ ProfileID: profileID, - EntityID: entityID, - RuleTypeName: *ruleType, - RuleName: *ruleName, + EntityID: maybeEntityID, + EntityName: maybeEntityName, + RuleTypeName: ruleType, + RuleName: ruleName, }) if err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, status.Errorf(codes.Unknown, "failed to list rule evaluation status: %s", err) @@ -663,11 +660,6 @@ func (s *Server) processProfileStatusByName( ) } - // Telemetry logging - entityCtx := engcontext.EntityFromContext(ctx) - logger.BusinessRecord(ctx).Project = entityCtx.Project.ID - logger.BusinessRecord(ctx).Profile = logger.Profile{Name: profileName, ID: profileID} - return &minderv1.GetProfileStatusByNameResponse{ ProfileStatus: &minderv1.ProfileStatus{ ProfileId: profileID.String(), @@ -689,22 +681,30 @@ func (s *Server) processProfileStatusById( ) (*minderv1.GetProfileStatusByIdResponse, error) { var ruleEvaluationStatuses []*minderv1.RuleEvaluationStatus - selector, ruleType, ruleName, err := extractFiltersFromIdRequest(req) - if err != nil { + // Telemetry logging + entityCtx := engcontext.EntityFromContext(ctx) + logger.BusinessRecord(ctx).Project = entityCtx.Project.ID + logger.BusinessRecord(ctx).Profile = logger.Profile{Name: profileName, ID: profileID} + + // selector, ruleType, ruleName, err := extractFiltersFromIdRequest(req) + if err := validateEntityType(req.GetEntity()); err != nil { return nil, err } + maybeEntityID, err := maybeNullUUID(req.GetEntity().GetId()) + if err != nil { + return nil, util.UserVisibleError(codes.InvalidArgument, "Unable to parse entity id: %q", req.GetEntity().GetId()) + } + maybeEntityName := maybeNullString(req.GetEntity().GetName()) + ruleType := maybeNullString(req.GetRuleType()) + ruleName := maybeNullString(req.GetRuleName()) - // Only fetch rule evaluations if selector is present or all is requested - if selector != nil || req.GetAll() { - var entityID uuid.NullUUID - if selector != nil { - entityID = *selector - } + if req.GetAll() || maybeEntityID.Valid || maybeEntityName.Valid { dbRuleEvaluationStatuses, err := s.store.ListRuleEvaluationsByProfileId(ctx, db.ListRuleEvaluationsByProfileIdParams{ ProfileID: profileID, - EntityID: entityID, - RuleTypeName: *ruleType, - RuleName: *ruleName, + EntityID: maybeEntityID, + EntityName: maybeEntityName, + RuleTypeName: ruleType, + RuleName: ruleName, }) if err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, status.Errorf(codes.Unknown, "failed to list rule evaluation status: %s", err) @@ -715,11 +715,6 @@ func (s *Server) processProfileStatusById( ) } - // Telemetry logging - entityCtx := engcontext.EntityFromContext(ctx) - logger.BusinessRecord(ctx).Project = entityCtx.Project.ID - logger.BusinessRecord(ctx).Profile = logger.Profile{Name: profileName, ID: profileID} - return &minderv1.GetProfileStatusByIdResponse{ ProfileStatus: &minderv1.ProfileStatus{ ProfileId: profileID.String(), @@ -731,61 +726,32 @@ func (s *Server) processProfileStatusById( }, nil } -func extractFiltersFromNameRequest( - req *minderv1.GetProfileStatusByNameRequest) ( - *uuid.NullUUID, *sql.NullString, *sql.NullString, error) { - if e := req.GetEntity(); e != nil { +func validateEntityType(e *minderv1.EntityTypedId) error { + if e != nil { if !e.GetType().IsValid() { - return nil, nil, nil, util.UserVisibleError(codes.InvalidArgument, + return util.UserVisibleError(codes.InvalidArgument, "invalid entity type %s, please use one of %s", e.GetType(), entities.KnownTypesCSV()) } } - - selector := extractEntitySelector(req.GetEntity()) - - ruleType := &sql.NullString{ - String: req.GetRuleType(), - Valid: req.GetRuleType() != "", - } - if !ruleType.Valid { - //nolint:staticcheck // ignore SA1019: Deprecated field supported for backward compatibility - ruleType = &sql.NullString{ - String: req.GetRule(), - Valid: req.GetRule() != "", - } - } - - ruleName := &sql.NullString{ - String: req.GetRuleName(), - Valid: req.GetRuleName() != "", - } - - return selector, ruleType, ruleName, nil + return nil } -func extractFiltersFromIdRequest( - req *minderv1.GetProfileStatusByIdRequest) ( - *uuid.NullUUID, *sql.NullString, *sql.NullString, error) { - if e := req.GetEntity(); e != nil { - if !e.GetType().IsValid() { - return nil, nil, nil, util.UserVisibleError(codes.InvalidArgument, - "invalid entity type %s, please use one of %s", - e.GetType(), entities.KnownTypesCSV()) - } - } - - selector := extractEntitySelector(req.GetEntity()) - - ruleType := &sql.NullString{ - String: req.GetRuleType(), - Valid: req.GetRuleType() != "", +func maybeNullString(s string) sql.NullString { + return sql.NullString{ + String: s, + Valid: s != "", } +} - ruleName := &sql.NullString{ - String: req.GetRuleName(), - Valid: req.GetRuleName() != "", +func maybeNullUUID(s string) (uuid.NullUUID, error) { + var id uuid.UUID + var err error + if s != "" { + id, err = uuid.Parse(s) } - - return selector, ruleType, ruleName, nil + return uuid.NullUUID{ + UUID: id, + Valid: s != "", + }, err } diff --git a/internal/controlplane/handlers_profile_test.go b/internal/controlplane/handlers_profile_test.go index 32f0fc5527..ca0a5072e2 100644 --- a/internal/controlplane/handlers_profile_test.go +++ b/internal/controlplane/handlers_profile_test.go @@ -1200,6 +1200,8 @@ func TestGetProfileStatusByName(t *testing.T) { require.Equal(t, dbProfile.ID.String(), resp.ProfileStatus.ProfileId, "Profile ID should match") require.Equal(t, expectedProfileName, resp.ProfileStatus.ProfileName, "Profile name should match") }) + + // TODO: add test case for requesting evaluation details } func TestGetProfileStatusById(t *testing.T) { @@ -1255,6 +1257,8 @@ func TestGetProfileStatusById(t *testing.T) { require.Equal(t, dbProfile.ID.String(), resp.ProfileStatus.ProfileId, "Profile ID should match") require.Equal(t, expectedProfileName, resp.ProfileStatus.ProfileName, "Profile name should match") }) + + // TODO: add test case for requesting evaluation details } type deleteProfileTestCase struct { diff --git a/internal/controlplane/handlers_reconciliationtasks.go b/internal/controlplane/handlers_reconciliationtasks.go index c083b1c971..9a6e008baf 100644 --- a/internal/controlplane/handlers_reconciliationtasks.go +++ b/internal/controlplane/handlers_reconciliationtasks.go @@ -18,6 +18,7 @@ import ( "github.com/mindersec/minder/internal/engine/engcontext" "github.com/mindersec/minder/internal/logger" reconcilers "github.com/mindersec/minder/internal/reconcilers/messages" + "github.com/mindersec/minder/internal/util" pb "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1" "github.com/mindersec/minder/pkg/eventer/constants" ) @@ -29,7 +30,7 @@ func (s *Server) CreateEntityReconciliationTask(ctx context.Context, ) { // Populated by EntityContextProjectInterceptor using incoming request entityCtx := engcontext.EntityFromContext(ctx) - err := entityCtx.Validate(ctx, s.store, s.providerStore) + dbProvider, err := entityCtx.Validate(ctx, s.store, s.providerStore) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "error in entity context: %v", err) } @@ -40,19 +41,39 @@ func (s *Server) CreateEntityReconciliationTask(ctx context.Context, return nil, status.Error(codes.InvalidArgument, "entity is required") } + // TODO: Support other entity types, replace with switch and update remainder. + if entity.GetType() != pb.Entity_ENTITY_REPOSITORIES { + return nil, status.Errorf(codes.InvalidArgument, "entity type %s is not supported", entity.GetType()) + } + entityType := db.EntitiesRepository + + if entity.GetId() == "" { + // Look up ID given name + dbEntity, err := s.store.GetEntityByName(ctx, db.GetEntityByNameParams{ + ProjectID: entityCtx.Project.ID, + EntityType: entityType, + Name: entity.GetName(), + ProviderID: dbProvider.ID, + }) + if err != nil { + return nil, util.UserVisibleError(codes.NotFound, + "Unable to find entity %q of type %s in provider %s", + entity.GetName(), + entityType, + entityCtx.Provider.Name, + ) + } + entity.Id = dbEntity.ID.String() + } + var msg *message.Message var topic string - // TODO: Support other entity types, replace with switch - if entity.GetType() == pb.Entity_ENTITY_REPOSITORIES { - msg, err = getRepositoryReconciliationMessage(ctx, s.store, entity.GetId(), entityCtx) - if err != nil { - return nil, err - } - topic = constants.TopicQueueReconcileRepoInit - } else { - return nil, status.Errorf(codes.InvalidArgument, "entity type %s is not supported", entity.GetType()) + msg, err = getRepositoryReconciliationMessage(ctx, s.store, entity.GetId(), entityCtx) + if err != nil { + return nil, err } + topic = constants.TopicQueueReconcileRepoInit // This is a non-fatal error, so we'll just log it and continue with the next ones if err := s.evt.Publish(topic, msg); err != nil { diff --git a/internal/controlplane/handlers_reconciliationtasks_test.go b/internal/controlplane/handlers_reconciliationtasks_test.go index 80d27c6679..b81af0a27b 100644 --- a/internal/controlplane/handlers_reconciliationtasks_test.go +++ b/internal/controlplane/handlers_reconciliationtasks_test.go @@ -52,7 +52,47 @@ func TestServer_CreateRepositoryReconciliationTask(t *testing.T) { setup: func(store *mockdb.MockStore, entityContext *engcontext.EntityContext) { projId := entityContext.Project.ID prov := entityContext.Provider.Name - setupTestingEntityContextValidation(store, projId, prov) + setupTestingEntityContextValidation(store, projId, prov, uuid.New()) + store.EXPECT(). + GetEntityByID(gomock.Any(), repoUuid). + Return(db.EntityInstance{ + ID: repoUuid, + EntityType: db.EntitiesRepository, + ProviderID: uuid.New(), + ProjectID: projId, + }, nil) + }, + err: "", + }, + { + name: "create reconciliation by name", + input: &pb.CreateEntityReconciliationTaskRequest{ + Entity: &pb.EntityTypedId{ + Type: pb.Entity_ENTITY_REPOSITORIES, + Name: "my/repo", + }, + }, + entityContext: &engcontext.EntityContext{ + Project: engcontext.Project{ + ID: uuid.New(), + }, + Provider: engcontext.Provider{ + Name: ghProvider, + }, + }, + setup: func(store *mockdb.MockStore, entityContext *engcontext.EntityContext) { + projId := entityContext.Project.ID + prov := entityContext.Provider.Name + provId := uuid.New() + setupTestingEntityContextValidation(store, projId, prov, provId) + store.EXPECT(). + GetEntityByName(gomock.Any(), db.GetEntityByNameParams{ + ProjectID: projId, + EntityType: "repository", + Name: "my/repo", + ProviderID: provId, + }). + Return(db.EntityInstance{ID: repoUuid}, nil) store.EXPECT(). GetEntityByID(gomock.Any(), repoUuid). Return(db.EntityInstance{ @@ -146,7 +186,7 @@ func TestServer_CreateRepositoryReconciliationTask(t *testing.T) { setup: func(store *mockdb.MockStore, entityContext *engcontext.EntityContext) { projId := entityContext.Project.ID prov := entityContext.Provider.Name - setupTestingEntityContextValidation(store, projId, prov) + setupTestingEntityContextValidation(store, projId, prov, uuid.New()) store.EXPECT(). GetEntityByID(gomock.Any(), repoUuid). Return(db.EntityInstance{}, sql.ErrNoRows) @@ -172,7 +212,7 @@ func TestServer_CreateRepositoryReconciliationTask(t *testing.T) { setup: func(store *mockdb.MockStore, entityContext *engcontext.EntityContext) { projId := entityContext.Project.ID prov := entityContext.Provider.Name - setupTestingEntityContextValidation(store, projId, prov) + setupTestingEntityContextValidation(store, projId, prov, uuid.New()) store.EXPECT(). GetEntityByID(gomock.Any(), repoUuid). Return(db.EntityInstance{}, sql.ErrConnDone) @@ -198,7 +238,7 @@ func TestServer_CreateRepositoryReconciliationTask(t *testing.T) { setup: func(store *mockdb.MockStore, entityContext *engcontext.EntityContext) { projId := entityContext.Project.ID prov := entityContext.Provider.Name - setupTestingEntityContextValidation(store, projId, prov) + setupTestingEntityContextValidation(store, projId, prov, uuid.New()) }, err: "error parsing repository id", }, @@ -220,7 +260,7 @@ func TestServer_CreateRepositoryReconciliationTask(t *testing.T) { setup: func(store *mockdb.MockStore, entityContext *engcontext.EntityContext) { projId := entityContext.Project.ID prov := entityContext.Provider.Name - setupTestingEntityContextValidation(store, projId, prov) + setupTestingEntityContextValidation(store, projId, prov, uuid.New()) }, err: "entity type ENTITY_UNSPECIFIED is not supported", }, @@ -240,7 +280,7 @@ func TestServer_CreateRepositoryReconciliationTask(t *testing.T) { setup: func(store *mockdb.MockStore, entityContext *engcontext.EntityContext) { projId := entityContext.Project.ID prov := entityContext.Provider.Name - setupTestingEntityContextValidation(store, projId, prov) + setupTestingEntityContextValidation(store, projId, prov, uuid.New()) }, err: "entity is required", }, @@ -283,7 +323,7 @@ func TestServer_CreateRepositoryReconciliationTask(t *testing.T) { } } -func setupTestingEntityContextValidation(store *mockdb.MockStore, projId uuid.UUID, prov string) { +func setupTestingEntityContextValidation(store *mockdb.MockStore, projId uuid.UUID, prov string, provId uuid.UUID) { store.EXPECT(). GetProjectByID(gomock.Any(), projId). Return(db.Project{ID: projId}, nil) @@ -295,5 +335,5 @@ func setupTestingEntityContextValidation(store *mockdb.MockStore, projId uuid.UU Name: sql.NullString{String: prov, Valid: true}, Projects: []uuid.UUID{projId}, Trait: db.NullProviderType{}, - }).Return([]db.Provider{{Name: prov}}, nil) + }).Return([]db.Provider{{Name: prov, ID: provId}}, nil) } diff --git a/internal/db/profile_status.sql.go b/internal/db/profile_status.sql.go index 149244afbb..0042a99939 100644 --- a/internal/db/profile_status.sql.go +++ b/internal/db/profile_status.sql.go @@ -249,6 +249,7 @@ SELECT rt.guidance as rule_type_guidance, rt.display_name as rule_type_display_name, ere.entity_instance_id as entity_id, + ei.name as entity_name, ei.project_id as project_id, rt.release_phase as rule_type_release_phase FROM latest_evaluation_statuses les @@ -260,15 +261,17 @@ FROM latest_evaluation_statuses les INNER JOIN rule_type rt ON rt.id = ri.rule_type_id INNER JOIN entity_instances ei ON ei.id = ere.entity_instance_id INNER JOIN providers prov ON prov.id = ei.provider_id -WHERE les.profile_id = $1 AND - (ere.entity_instance_id = $2::UUID OR $2::UUID IS NULL) - AND (rt.name = $3 OR $3 IS NULL) - AND (lower(ri.name) = lower($4) OR $4 IS NULL) +WHERE les.profile_id = $1 + AND (ere.entity_instance_id = $2::UUID OR $2::UUID IS NULL) + AND (ei.name = $3 OR $3 IS NULL) + AND (rt.name = $4 OR $4 IS NULL) + AND (lower(ri.name) = lower($5) OR $5 IS NULL) ` type ListRuleEvaluationsByProfileIdParams struct { ProfileID uuid.UUID `json:"profile_id"` EntityID uuid.NullUUID `json:"entity_id"` + EntityName sql.NullString `json:"entity_name"` RuleTypeName sql.NullString `json:"rule_type_name"` RuleName sql.NullString `json:"rule_name"` } @@ -295,6 +298,7 @@ type ListRuleEvaluationsByProfileIdRow struct { RuleTypeGuidance string `json:"rule_type_guidance"` RuleTypeDisplayName string `json:"rule_type_display_name"` EntityID uuid.UUID `json:"entity_id"` + EntityName string `json:"entity_name"` ProjectID uuid.UUID `json:"project_id"` RuleTypeReleasePhase ReleaseStatus `json:"rule_type_release_phase"` } @@ -303,6 +307,7 @@ func (q *Queries) ListRuleEvaluationsByProfileId(ctx context.Context, arg ListRu rows, err := q.db.QueryContext(ctx, listRuleEvaluationsByProfileId, arg.ProfileID, arg.EntityID, + arg.EntityName, arg.RuleTypeName, arg.RuleName, ) @@ -335,6 +340,7 @@ func (q *Queries) ListRuleEvaluationsByProfileId(ctx context.Context, arg ListRu &i.RuleTypeGuidance, &i.RuleTypeDisplayName, &i.EntityID, + &i.EntityName, &i.ProjectID, &i.RuleTypeReleasePhase, ); err != nil { diff --git a/internal/engine/engcontext/context.go b/internal/engine/engcontext/context.go index 60d4b64175..fbf6b8a6b4 100644 --- a/internal/engine/engcontext/context.go +++ b/internal/engine/engcontext/context.go @@ -56,18 +56,18 @@ type EntityContext struct { } // Validate validates that the entity context contains values that are present in the DB -func (c *EntityContext) Validate(ctx context.Context, q db.Querier, providerStore providers.ProviderStore) error { +func (c *EntityContext) Validate(ctx context.Context, q db.Querier, providerStore providers.ProviderStore) (*db.Provider, error) { _, err := q.GetProjectByID(ctx, c.Project.ID) if err != nil { - return fmt.Errorf("unable to get context: failed getting project: %w", err) + return nil, fmt.Errorf("unable to get context: failed getting project: %w", err) } - _, err = providerStore.GetByName(ctx, c.Project.ID, c.Provider.Name) + dbProvider, err := providerStore.GetByName(ctx, c.Project.ID, c.Provider.Name) if err != nil { - return fmt.Errorf("unable to get context: failed getting provider: %w", err) + return nil, fmt.Errorf("unable to get context: failed getting provider: %w", err) } - return nil + return dbProvider, nil } // ValidateProject validates that the entity context contains a project that is present in the DB diff --git a/pkg/api/openapi/minder/v1/minder.swagger.json b/pkg/api/openapi/minder/v1/minder.swagger.json index d2a6c7f9f3..02187440b9 100644 --- a/pkg/api/openapi/minder/v1/minder.swagger.json +++ b/pkg/api/openapi/minder/v1/minder.swagger.json @@ -1573,7 +1573,7 @@ }, { "name": "entity.type", - "description": "entity is the entity to get status for. Incompatible with `all`", + "description": "entity is the entity to get status for. Incompatible with `all`\n\nOn input, at least one of id and name must be set. If both are set, they must both match.\n On output, both id and name will be set.", "in": "query", "required": true, "type": "string", @@ -1594,7 +1594,14 @@ "name": "entity.id", "description": "id is the ID of the entity to get status for. Incompatible with `all`", "in": "query", - "required": true, + "required": false, + "type": "string" + }, + { + "name": "entity.name", + "description": "name is the name of the entity. This name is unique within a given project, type, and provider, but may not be globally unique.", + "in": "query", + "required": false, "type": "string" }, { @@ -1812,7 +1819,7 @@ }, { "name": "entity.type", - "description": "entity is the entity to get status for. Incompatible with `all`", + "description": "entity is the entity to get status for. Incompatible with `all`\n\nOn input, at least one of id and name must be set. If both are set, they must both match.\n On output, both id and name will be set.", "in": "query", "required": true, "type": "string", @@ -1833,7 +1840,14 @@ "name": "entity.id", "description": "id is the ID of the entity to get status for. Incompatible with `all`", "in": "query", - "required": true, + "required": false, + "type": "string" + }, + { + "name": "entity.name", + "description": "name is the name of the entity. This name is unique within a given project, type, and provider, but may not be globally unique.", + "in": "query", + "required": false, "type": "string" }, { @@ -4496,17 +4510,21 @@ "properties": { "type": { "$ref": "#/definitions/v1Entity", + "description": "On input, at least one of id and name must be set. If both are set, they must both match.\n On output, both id and name will be set.", "title": "entity is the entity to get status for. Incompatible with `all`" }, "id": { "type": "string", "title": "id is the ID of the entity to get status for. Incompatible with `all`" + }, + "name": { + "type": "string", + "description": "name is the name of the entity. This name is unique within a given project, type, and provider, but may not be globally unique." } }, - "description": "EntiryTypeId is a message that carries an ID together with a type to uniquely identify an entity\nsuch as (repo, 1), (artifact, 2), ...", + "description": "EntityTypedId is a message that carries an ID together with a type to uniquely identify an entity\nsuch as (repo, 1), (artifact, 2), ...", "required": [ - "type", - "id" + "type" ] }, "v1EvalResultAlert": { diff --git a/pkg/api/protobuf/go/minder/v1/minder.pb.go b/pkg/api/protobuf/go/minder/v1/minder.pb.go index 10f8ade4f3..3b12149107 100644 --- a/pkg/api/protobuf/go/minder/v1/minder.pb.go +++ b/pkg/api/protobuf/go/minder/v1/minder.pb.go @@ -5702,14 +5702,16 @@ func (x *RuleEvaluationStatus) GetReleasePhase() RuleTypeReleasePhase { return RuleTypeReleasePhase_RULE_TYPE_RELEASE_PHASE_UNSPECIFIED } -// EntiryTypeId is a message that carries an ID together with a type to uniquely identify an entity +// EntityTypedId is a message that carries an ID together with a type to uniquely identify an entity // such as (repo, 1), (artifact, 2), ... type EntityTypedId struct { state protoimpl.MessageState `protogen:"open.v1"` // entity is the entity to get status for. Incompatible with `all` Type Entity `protobuf:"varint,1,opt,name=type,proto3,enum=minder.v1.Entity" json:"type,omitempty"` // id is the ID of the entity to get status for. Incompatible with `all` - Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` + Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` + // name is the name of the entity. This name is unique within a given project, type, and provider, but may not be globally unique. + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -5758,6 +5760,13 @@ func (x *EntityTypedId) GetId() string { return "" } +func (x *EntityTypedId) GetName() string { + if x != nil { + return x.Name + } + return "" +} + type GetProfileStatusByNameRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // context is the context in which the rule type is evaluated. @@ -15097,10 +15106,11 @@ const file_minder_v1_minder_proto_rawDesc = "" + "\x0fEntityInfoEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x1b\n" + - "\x19_remediation_last_updated\"X\n" + + "\x19_remediation_last_updated\"\x94\x01\n" + "\rEntityTypedId\x12*\n" + "\x04type\x18\x01 \x01(\x0e2\x11.minder.v1.EntityB\x03\xe0A\x02R\x04type\x12\x1b\n" + - "\x02id\x18\x02 \x01(\tB\v\xe0A\x02\xbaH\x05r\x03\xb0\x01\x01R\x02id\"\xee\x02\n" + + "\x02id\x18\x02 \x01(\tB\v\xe0A\x01\xbaH\x05r\x03\xb0\x01\x01R\x02id\x12:\n" + + "\x04name\x18\x03 \x01(\tB&\xe0A\x01\xbaH r\x1e(\xc8\x012\x19^[[:alnum:]][-/[:word:]]*R\x04name\"\xee\x02\n" + "\x1dGetProfileStatusByNameRequest\x12,\n" + "\acontext\x18\x01 \x01(\v2\x12.minder.v1.ContextR\acontext\x128\n" + "\x04name\x18\x02 \x01(\tB$\xbaH!\xd8\x01\x01r\x1c\x18\xc8\x012\x17^[A-Za-z][-/[:word:]]*$R\x04name\x120\n" + diff --git a/proto/minder/v1/minder.proto b/proto/minder/v1/minder.proto index a2385520ef..ead12b3fcd 100644 --- a/proto/minder/v1/minder.proto +++ b/proto/minder/v1/minder.proto @@ -1883,17 +1883,28 @@ message RuleEvaluationStatus { ]; } -// EntiryTypeId is a message that carries an ID together with a type to uniquely identify an entity +// EntityTypedId is a message that carries an ID together with a type to uniquely identify an entity // such as (repo, 1), (artifact, 2), ... message EntityTypedId { // entity is the entity to get status for. Incompatible with `all` Entity type = 1 [ (google.api.field_behavior) = REQUIRED ]; + // On input, at least one of id and name must be set. If both are set, they must both match. + // On output, both id and name will be set. + // id is the ID of the entity to get status for. Incompatible with `all` string id = 2 [ (buf.validate.field).string = {uuid: true}, - (google.api.field_behavior) = REQUIRED + (google.api.field_behavior) = OPTIONAL + ]; + // name is the name of the entity. This name is unique within a given project, type, and provider, but may not be globally unique. + string name = 3 [ + (buf.validate.field).string = { + max_bytes: 200 + pattern: "^[[:alnum:]][-/[:word:]]*" + }, + (google.api.field_behavior) = OPTIONAL ]; }