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
];
}