Skip to content

Commit

Permalink
refactor: Move webhook event parsinglogic to webhook handler
Browse files Browse the repository at this point in the history
This moves the event parsing logic to the webhook handler since that's
already github specific. Instead of doing any JSON mangling on the executor
side, it will expect already built events that are ready to be processed.

This makes the executor lighter and allows us to add more
complex logic on the webhook side (e.g. handling pull requests)
  • Loading branch information
JAORMX committed Sep 5, 2023
1 parent f171e5a commit e265567
Show file tree
Hide file tree
Showing 12 changed files with 1,054 additions and 821 deletions.
175 changes: 175 additions & 0 deletions internal/engine/entity_event.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// Copyright 2023 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.role/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 engine

import (
"fmt"

"github.com/ThreeDotsLabs/watermill/message"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/reflect/protoreflect"

"github.com/stacklok/mediator/internal/util"
"github.com/stacklok/mediator/pkg/entities"
pb "github.com/stacklok/mediator/pkg/generated/protobuf/go/mediator/v1"
)

// entityInfoWrapper is a helper struct to gather information
// about entities from events.
// It assumes that the message.Message contains a payload
// with a protobuf message that's specific to the entity type.
//
// It also assumes the following metadata keys are present:
//
// - EntityTypeEventKey - entity_type
// - GroupIDEventKey - group_id
// - RepositoryIDEventKey - repository_id
// - ArtifactIDEventKey - artifact_id (only for versioned artifacts)
//
// Entity type is used to determine the type of the protobuf message
// and the entity type in the database. It may be one of the following:
//
// - RepositoryEventEntityType - repository
// - VersionedArtifactEventEntityType - versioned_artifact
type entityInfoWrapper struct {
GroupID int32
Entity protoreflect.ProtoMessage
Type pb.Entity
OwnershipData map[string]int32
}

const (
// RepositoryEventEntityType is the entity type for repositories
RepositoryEventEntityType = "repository"
// VersionedArtifactEventEntityType is the entity type for versioned artifacts
VersionedArtifactEventEntityType = "versioned_artifact"
)

const (
// EntityTypeEventKey is the key for the entity type
EntityTypeEventKey = "entity_type"
// GroupIDEventKey is the key for the group ID
GroupIDEventKey = "group_id"
// RepositoryIDEventKey is the key for the repository ID
RepositoryIDEventKey = "repository_id"
// ArtifactIDEventKey is the key for the artifact ID
ArtifactIDEventKey = "artifact_id"
)

func parseEntityEvent(msg *message.Message) (*entityInfoWrapper, error) {
out := &entityInfoWrapper{
OwnershipData: make(map[string]int32),
}

if err := out.withGroupIDFromMessage(msg); err != nil {
return nil, err
}

// We always have the repository ID.
if err := out.withRepositoryIDFromMessage(msg); err != nil {
return nil, err
}

typ := msg.Metadata.Get(EntityTypeEventKey)
switch typ {
case RepositoryEventEntityType:
out.asRepository()
case VersionedArtifactEventEntityType:
out.asVersionedArtifact()
if err := out.withArtifactIDFromMessage(msg); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unknown entity type: %s", typ)
}

if err := out.unmarshalEntity(msg); err != nil {
return nil, fmt.Errorf("error unmarshalling payload: %w", err)
}

return out, nil
}

func (eiw *entityInfoWrapper) asRepository() {
eiw.Type = pb.Entity_ENTITY_REPOSITORIES
eiw.Entity = &pb.RepositoryResult{}
}

func (eiw *entityInfoWrapper) asVersionedArtifact() {
eiw.Type = pb.Entity_ENTITY_ARTIFACTS
eiw.Entity = &pb.VersionedArtifact{}
}

func (eiw *entityInfoWrapper) withGroupIDFromMessage(msg *message.Message) error {
id, err := getIDFromMessage(msg, GroupIDEventKey)
if err != nil {
return fmt.Errorf("error parsing %s: %w", GroupIDEventKey, err)
}

eiw.GroupID = id
return nil
}

func (eiw *entityInfoWrapper) withRepositoryIDFromMessage(msg *message.Message) error {
return eiw.withIDFromMessage(msg, RepositoryIDEventKey)
}

func (eiw *entityInfoWrapper) withArtifactIDFromMessage(msg *message.Message) error {
return eiw.withIDFromMessage(msg, ArtifactIDEventKey)
}

func (eiw *entityInfoWrapper) withIDFromMessage(msg *message.Message, key string) error {
id, err := getIDFromMessage(msg, key)
if err != nil {
return fmt.Errorf("error parsing %s: %w", key, err)
}

eiw.OwnershipData[key] = id
return nil
}

func (eiw *entityInfoWrapper) unmarshalEntity(msg *message.Message) error {
return protojson.Unmarshal(msg.Payload, eiw.Entity)
}

func (eiw *entityInfoWrapper) evalStatusParams(
policyID int32,
ruleTypeID int32,
evalErr error,
) *createOrUpdateEvalStatusParams {
params := &createOrUpdateEvalStatusParams{
policyID: policyID,
repoID: eiw.OwnershipData[RepositoryIDEventKey],
ruleTypeEntity: entities.EntityTypeToDB(eiw.Type),
ruleTypeID: ruleTypeID,
evalErr: evalErr,
}

artifactID, ok := eiw.OwnershipData[ArtifactIDEventKey]
if ok {
params.artifactID = artifactID
}

return params
}

func getIDFromMessage(msg *message.Message, key string) (int32, error) {
rawID := msg.Metadata.Get(key)
if rawID == "" {
return 0, fmt.Errorf("%s not found in metadata", key)
}

return util.Int32FromString(rawID)
}
94 changes: 94 additions & 0 deletions internal/engine/eval_status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright 2023 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.role/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 engine

import (
"context"
"database/sql"
"errors"
"fmt"
"log"

evalerrors "github.com/stacklok/mediator/internal/engine/errors"
"github.com/stacklok/mediator/pkg/db"
)

// createOrUpdateEvalStatusParams is a helper struct to pass parameters to createOrUpdateEvalStatus
// to avoid confusion with the parameters order. Since at the moment all our entities are bound to
// a repo and most policies are expecting a repo, the repoID parameter is mandatory. For entities
// other than artifacts, the artifactID should be 0 which is translated to NULL in the database.
type createOrUpdateEvalStatusParams struct {
policyID int32
repoID int32
artifactID int32
ruleTypeEntity db.Entities
ruleTypeID int32
evalErr error
}

func (e *Executor) createOrUpdateEvalStatus(
ctx context.Context,
params *createOrUpdateEvalStatusParams,
) error {
if params == nil {
return fmt.Errorf("createOrUpdateEvalStatusParams cannot be nil")
}

if errors.Is(params.evalErr, evalerrors.ErrEvaluationSkipSilently) {
log.Printf("silent skip of rule %d for policy %d for entity %s in repo %d",
params.ruleTypeID, params.policyID, params.ruleTypeEntity, params.repoID)
return nil
}

var sqlArtifactID sql.NullInt32
if params.artifactID > 0 {
sqlArtifactID = sql.NullInt32{
Int32: params.artifactID,
Valid: true,
}
}

return e.querier.UpsertRuleEvaluationStatus(ctx, db.UpsertRuleEvaluationStatusParams{
PolicyID: params.policyID,
RepositoryID: sql.NullInt32{
Int32: params.repoID,
Valid: true,
},
ArtifactID: sqlArtifactID,
Entity: params.ruleTypeEntity,
RuleTypeID: params.ruleTypeID,
EvalStatus: errorAsEvalStatus(params.evalErr),
Details: errorAsDetails(params.evalErr),
})
}

func errorAsEvalStatus(err error) db.EvalStatusTypes {
if errors.Is(err, evalerrors.ErrEvaluationFailed) {
return db.EvalStatusTypesFailure
} else if errors.Is(err, evalerrors.ErrEvaluationSkipped) {
return db.EvalStatusTypesSkipped
} else if err != nil {
return db.EvalStatusTypesError
}
return db.EvalStatusTypesSuccess
}

func errorAsDetails(err error) string {
if err != nil {
return err.Error()
}

return ""
}
Loading

0 comments on commit e265567

Please sign in to comment.