mirror of https://github.com/harness/drone.git
235 lines
6.7 KiB
Go
235 lines
6.7 KiB
Go
// Copyright 2023 Harness, Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package check
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"time"
|
|
|
|
"github.com/harness/gitness/app/api/usererror"
|
|
"github.com/harness/gitness/app/auth"
|
|
"github.com/harness/gitness/git"
|
|
"github.com/harness/gitness/store"
|
|
"github.com/harness/gitness/types"
|
|
"github.com/harness/gitness/types/enum"
|
|
)
|
|
|
|
type ReportInput struct {
|
|
// TODO [CODE-1363]: remove after identifier migration.
|
|
CheckUID string `json:"check_uid" deprecated:"true"`
|
|
Identifier string `json:"identifier"`
|
|
Status enum.CheckStatus `json:"status"`
|
|
Summary string `json:"summary"`
|
|
Link string `json:"link"`
|
|
Payload types.CheckPayload `json:"payload"`
|
|
|
|
Started int64 `json:"started,omitempty"`
|
|
Ended int64 `json:"ended,omitempty"`
|
|
}
|
|
|
|
// TODO: Can we drop the '$' - depends on whether harness allows it.
|
|
var regexpCheckIdentifier = "^[0-9a-zA-Z-_.$]{1,127}$"
|
|
var matcherCheckIdentifier = regexp.MustCompile(regexpCheckIdentifier)
|
|
|
|
// Sanitize validates and sanitizes the ReportInput data.
|
|
func (in *ReportInput) Sanitize(
|
|
sanitizers map[enum.CheckPayloadKind]func(in *ReportInput, s *auth.Session) error, session *auth.Session,
|
|
) error {
|
|
// TODO [CODE-1363]: remove after identifier migration.
|
|
if in.Identifier == "" {
|
|
in.Identifier = in.CheckUID
|
|
}
|
|
|
|
if in.Identifier == "" {
|
|
return usererror.BadRequest("Identifier is missing")
|
|
}
|
|
|
|
if !matcherCheckIdentifier.MatchString(in.Identifier) {
|
|
return usererror.BadRequestf("Identifier must match the regular expression: %s", regexpCheckIdentifier)
|
|
}
|
|
|
|
_, ok := in.Status.Sanitize()
|
|
if !ok {
|
|
return usererror.BadRequest("Invalid value provided for status check status")
|
|
}
|
|
|
|
validatorFn, ok := sanitizers[in.Payload.Kind]
|
|
if !ok {
|
|
return usererror.BadRequest("Invalid value provided for the payload kind")
|
|
}
|
|
|
|
// Validate and sanitize the input data based on version; Require a link... and similar operations.
|
|
if err := validatorFn(in, session); err != nil {
|
|
return fmt.Errorf("payload validation failed: %w", err)
|
|
}
|
|
|
|
if in.Ended != 0 && in.Ended < in.Started {
|
|
return usererror.BadRequest("started time reported after ended time")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func SanitizeJSONPayload(source json.RawMessage, data any) (json.RawMessage, error) {
|
|
if len(source) == 0 {
|
|
return json.Marshal(data) // marshal the empty object
|
|
}
|
|
|
|
decoder := json.NewDecoder(bytes.NewReader(source))
|
|
decoder.DisallowUnknownFields()
|
|
|
|
if err := decoder.Decode(&data); err != nil {
|
|
return nil, usererror.BadRequestf("Payload data doesn't match the required format: %s", err.Error())
|
|
}
|
|
|
|
buffer := bytes.NewBuffer(nil)
|
|
buffer.Grow(512)
|
|
|
|
encoder := json.NewEncoder(buffer)
|
|
encoder.SetEscapeHTML(false)
|
|
if err := encoder.Encode(data); err != nil {
|
|
return nil, fmt.Errorf("failed to sanitize json payload: %w", err)
|
|
}
|
|
|
|
result := buffer.Bytes()
|
|
|
|
if result[len(result)-1] == '\n' {
|
|
result = result[:len(result)-1]
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// Report modifies an existing or creates a new (if none yet exists) status check report for a specific commit.
|
|
func (c *Controller) Report(
|
|
ctx context.Context,
|
|
session *auth.Session,
|
|
repoRef string,
|
|
commitSHA string,
|
|
in *ReportInput,
|
|
metadata map[string]string,
|
|
) (*types.Check, error) {
|
|
repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoReportCommitCheck)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to acquire access access to repo: %w", err)
|
|
}
|
|
|
|
if errValidate := in.Sanitize(c.sanitizers, session); errValidate != nil {
|
|
return nil, errValidate
|
|
}
|
|
|
|
if !git.ValidateCommitSHA(commitSHA) {
|
|
return nil, usererror.BadRequest("invalid commit SHA provided")
|
|
}
|
|
|
|
_, err = c.git.GetCommit(ctx, &git.GetCommitParams{
|
|
ReadParams: git.ReadParams{RepoUID: repo.GitUID},
|
|
SHA: commitSHA,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to commit sha=%s: %w", commitSHA, err)
|
|
}
|
|
|
|
now := time.Now().UnixMilli()
|
|
|
|
metadataJSON, _ := json.Marshal(metadata)
|
|
|
|
existingCheck, err := c.checkStore.FindByIdentifier(ctx, repo.ID, commitSHA, in.Identifier)
|
|
|
|
if err != nil && !errors.Is(err, store.ErrResourceNotFound) {
|
|
return nil, fmt.Errorf("failed to find existing check for Identifier %q: %w", in.Identifier, err)
|
|
}
|
|
|
|
started := getStartTime(in, existingCheck, now)
|
|
ended := getEndTime(in, now)
|
|
|
|
statusCheckReport := &types.Check{
|
|
CreatedBy: session.Principal.ID,
|
|
Created: now,
|
|
Updated: now,
|
|
RepoID: repo.ID,
|
|
CommitSHA: commitSHA,
|
|
Identifier: in.Identifier,
|
|
Status: in.Status,
|
|
Summary: in.Summary,
|
|
Link: in.Link,
|
|
Payload: in.Payload,
|
|
Metadata: metadataJSON,
|
|
ReportedBy: *session.Principal.ToPrincipalInfo(),
|
|
Started: started,
|
|
Ended: ended,
|
|
}
|
|
|
|
err = c.checkStore.Upsert(ctx, statusCheckReport)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to upsert status check result for repo=%s: %w", repo.Identifier, err)
|
|
}
|
|
|
|
return statusCheckReport, nil
|
|
}
|
|
|
|
func getStartTime(in *ReportInput, check types.Check, now int64) int64 {
|
|
// start value came in api
|
|
if in.Started != 0 {
|
|
return in.Started
|
|
}
|
|
// in.started has no value we smartly put value for started
|
|
|
|
// in case of pending we assume check has not started running
|
|
if in.Status == enum.CheckStatusPending {
|
|
return 0
|
|
}
|
|
|
|
// new check
|
|
if check.Started == 0 {
|
|
return now
|
|
}
|
|
|
|
// The incoming check status can now be running or terminal.
|
|
|
|
// in case we already have running status we don't update time else we return current time as check has started
|
|
// running.
|
|
if check.Status == enum.CheckStatusRunning {
|
|
return check.Started
|
|
}
|
|
|
|
// Note: In case of reporting terminal statuses again and again we have assumed its
|
|
// a report of new status check everytime.
|
|
|
|
// In case someone reports any status before marking running return current time.
|
|
// This can happen if someone only reports terminal status or marks running status again after terminal.
|
|
return now
|
|
}
|
|
|
|
func getEndTime(in *ReportInput, now int64) int64 {
|
|
// end value came in api
|
|
if in.Ended != 0 {
|
|
return in.Ended
|
|
}
|
|
|
|
// if we get terminal status i.e. error, failure or success we return current time.
|
|
if in.Status.IsCompleted() {
|
|
return now
|
|
}
|
|
|
|
// in case of other status we return value as 0, which means we have not yet completed the check.
|
|
return 0
|
|
}
|