drone/app/store/database/execution.go

376 lines
11 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 database
import (
"context"
"fmt"
"time"
"github.com/harness/gitness/app/store"
gitness_store "github.com/harness/gitness/store"
"github.com/harness/gitness/store/database"
"github.com/harness/gitness/store/database/dbtx"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
"github.com/jmoiron/sqlx"
sqlxtypes "github.com/jmoiron/sqlx/types"
"github.com/pkg/errors"
)
var _ store.ExecutionStore = (*executionStore)(nil)
// NewExecutionStore returns a new ExecutionStore.
func NewExecutionStore(db *sqlx.DB) store.ExecutionStore {
return &executionStore{
db: db,
}
}
type executionStore struct {
db *sqlx.DB
}
// execution represents an execution object stored in the database.
type execution struct {
ID int64 `db:"execution_id"`
PipelineID int64 `db:"execution_pipeline_id"`
CreatedBy int64 `db:"execution_created_by"`
RepoID int64 `db:"execution_repo_id"`
Trigger string `db:"execution_trigger"`
Number int64 `db:"execution_number"`
Parent int64 `db:"execution_parent"`
Status enum.CIStatus `db:"execution_status"`
Error string `db:"execution_error"`
Event enum.TriggerEvent `db:"execution_event"`
Action enum.TriggerAction `db:"execution_action"`
Link string `db:"execution_link"`
Timestamp int64 `db:"execution_timestamp"`
Title string `db:"execution_title"`
Message string `db:"execution_message"`
Before string `db:"execution_before"`
After string `db:"execution_after"`
Ref string `db:"execution_ref"`
Fork string `db:"execution_source_repo"`
Source string `db:"execution_source"`
Target string `db:"execution_target"`
Author string `db:"execution_author"`
AuthorName string `db:"execution_author_name"`
AuthorEmail string `db:"execution_author_email"`
AuthorAvatar string `db:"execution_author_avatar"`
Sender string `db:"execution_sender"`
Params sqlxtypes.JSONText `db:"execution_params"`
Cron string `db:"execution_cron"`
Deploy string `db:"execution_deploy"`
DeployID int64 `db:"execution_deploy_id"`
Debug bool `db:"execution_debug"`
Started int64 `db:"execution_started"`
Finished int64 `db:"execution_finished"`
Created int64 `db:"execution_created"`
Updated int64 `db:"execution_updated"`
Version int64 `db:"execution_version"`
}
const (
executionColumns = `
execution_id
,execution_pipeline_id
,execution_created_by
,execution_repo_id
,execution_trigger
,execution_number
,execution_parent
,execution_status
,execution_error
,execution_event
,execution_action
,execution_link
,execution_timestamp
,execution_title
,execution_message
,execution_before
,execution_after
,execution_ref
,execution_source_repo
,execution_source
,execution_target
,execution_author
,execution_author_name
,execution_author_email
,execution_author_avatar
,execution_sender
,execution_params
,execution_cron
,execution_deploy
,execution_deploy_id
,execution_debug
,execution_started
,execution_finished
,execution_created
,execution_updated
,execution_version
`
)
// Find returns an execution given an execution ID.
func (s *executionStore) Find(ctx context.Context, id int64) (*types.Execution, error) {
//nolint:goconst
const findQueryStmt = `
SELECT` + executionColumns + `
FROM executions
WHERE execution_id = $1`
db := dbtx.GetAccessor(ctx, s.db)
dst := new(execution)
if err := db.GetContext(ctx, dst, findQueryStmt, id); err != nil {
return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find execution")
}
return mapInternalToExecution(dst)
}
// FindByNumber returns an execution given a pipeline ID and an execution number.
func (s *executionStore) FindByNumber(
ctx context.Context,
pipelineID int64,
executionNum int64,
) (*types.Execution, error) {
const findQueryStmt = `
SELECT` + executionColumns + `
FROM executions
WHERE execution_pipeline_id = $1 AND execution_number = $2`
db := dbtx.GetAccessor(ctx, s.db)
dst := new(execution)
if err := db.GetContext(ctx, dst, findQueryStmt, pipelineID, executionNum); err != nil {
return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find execution")
}
return mapInternalToExecution(dst)
}
// Create creates a new execution in the datastore.
func (s *executionStore) Create(ctx context.Context, execution *types.Execution) error {
const executionInsertStmt = `
INSERT INTO executions (
execution_pipeline_id
,execution_repo_id
,execution_created_by
,execution_trigger
,execution_number
,execution_parent
,execution_status
,execution_error
,execution_event
,execution_action
,execution_link
,execution_timestamp
,execution_title
,execution_message
,execution_before
,execution_after
,execution_ref
,execution_source_repo
,execution_source
,execution_target
,execution_author
,execution_author_name
,execution_author_email
,execution_author_avatar
,execution_sender
,execution_params
,execution_cron
,execution_deploy
,execution_deploy_id
,execution_debug
,execution_started
,execution_finished
,execution_created
,execution_updated
,execution_version
) VALUES (
:execution_pipeline_id
,:execution_repo_id
,:execution_created_by
,:execution_trigger
,:execution_number
,:execution_parent
,:execution_status
,:execution_error
,:execution_event
,:execution_action
,:execution_link
,:execution_timestamp
,:execution_title
,:execution_message
,:execution_before
,:execution_after
,:execution_ref
,:execution_source_repo
,:execution_source
,:execution_target
,:execution_author
,:execution_author_name
,:execution_author_email
,:execution_author_avatar
,:execution_sender
,:execution_params
,:execution_cron
,:execution_deploy
,:execution_deploy_id
,:execution_debug
,:execution_started
,:execution_finished
,:execution_created
,:execution_updated
,:execution_version
) RETURNING execution_id`
db := dbtx.GetAccessor(ctx, s.db)
query, arg, err := db.BindNamed(executionInsertStmt, mapExecutionToInternal(execution))
if err != nil {
return database.ProcessSQLErrorf(ctx, err, "Failed to bind execution object")
}
if err = db.QueryRowContext(ctx, query, arg...).Scan(&execution.ID); err != nil {
return database.ProcessSQLErrorf(ctx, err, "Execution query failed")
}
return nil
}
// Update tries to update an execution in the datastore with optimistic locking.
func (s *executionStore) Update(ctx context.Context, e *types.Execution) error {
const executionUpdateStmt = `
UPDATE executions
SET
execution_status = :execution_status
,execution_error = :execution_error
,execution_event = :execution_event
,execution_started = :execution_started
,execution_finished = :execution_finished
,execution_updated = :execution_updated
,execution_version = :execution_version
WHERE execution_id = :execution_id AND execution_version = :execution_version - 1`
updatedAt := time.Now()
stages := e.Stages
execution := mapExecutionToInternal(e)
execution.Version++
execution.Updated = updatedAt.UnixMilli()
db := dbtx.GetAccessor(ctx, s.db)
query, arg, err := db.BindNamed(executionUpdateStmt, execution)
if err != nil {
return database.ProcessSQLErrorf(ctx, err, "Failed to bind execution object")
}
result, err := db.ExecContext(ctx, query, arg...)
if err != nil {
return database.ProcessSQLErrorf(ctx, err, "Failed to update execution")
}
count, err := result.RowsAffected()
if err != nil {
return database.ProcessSQLErrorf(ctx, err, "Failed to get number of updated rows")
}
if count == 0 {
return gitness_store.ErrVersionConflict
}
m, err := mapInternalToExecution(execution)
if err != nil {
return fmt.Errorf("could not map execution object: %w", err)
}
*e = *m
e.Version = execution.Version
e.Updated = execution.Updated
e.Stages = stages // stages are not mapped in database.
return nil
}
// List lists the executions for a given pipeline ID.
// It orders them in descending order of execution number.
func (s *executionStore) List(
ctx context.Context,
pipelineID int64,
pagination types.Pagination,
) ([]*types.Execution, error) {
stmt := database.Builder.
Select(executionColumns).
From("executions").
Where("execution_pipeline_id = ?", fmt.Sprint(pipelineID)).
OrderBy("execution_number " + enum.OrderDesc.String())
stmt = stmt.Limit(database.Limit(pagination.Size))
stmt = stmt.Offset(database.Offset(pagination.Page, pagination.Size))
sql, args, err := stmt.ToSql()
if err != nil {
return nil, errors.Wrap(err, "Failed to convert query to sql")
}
db := dbtx.GetAccessor(ctx, s.db)
dst := []*execution{}
if err = db.SelectContext(ctx, &dst, sql, args...); err != nil {
return nil, database.ProcessSQLErrorf(ctx, err, "Failed executing custom list query")
}
return mapInternalToExecutionList(dst)
}
// Count of executions in a pipeline, if pipelineID is 0 then return total number of executions.
func (s *executionStore) Count(ctx context.Context, pipelineID int64) (int64, error) {
stmt := database.Builder.
Select("count(*)").
From("executions")
if pipelineID > 0 {
stmt = stmt.Where("execution_pipeline_id = ?", pipelineID)
}
sql, args, err := stmt.ToSql()
if err != nil {
return 0, errors.Wrap(err, "Failed to convert query to sql")
}
db := dbtx.GetAccessor(ctx, s.db)
var count int64
err = db.QueryRowContext(ctx, sql, args...).Scan(&count)
if err != nil {
return 0, database.ProcessSQLErrorf(ctx, err, "Failed executing count query")
}
return count, nil
}
// Delete deletes an execution given a pipeline ID and an execution number.
func (s *executionStore) Delete(ctx context.Context, pipelineID int64, executionNum int64) error {
const executionDeleteStmt = `
DELETE FROM executions
WHERE execution_pipeline_id = $1 AND execution_number = $2`
db := dbtx.GetAccessor(ctx, s.db)
if _, err := db.ExecContext(ctx, executionDeleteStmt, pipelineID, executionNum); err != nil {
return database.ProcessSQLErrorf(ctx, err, "Could not delete execution")
}
return nil
}