drone/app/store/database/gitspace_instance.go

399 lines
13 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"
"database/sql"
"fmt"
"strings"
"github.com/harness/gitness/app/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/Masterminds/squirrel"
"github.com/guregu/null"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
)
var _ store.GitspaceInstanceStore = (*gitspaceInstanceStore)(nil)
const (
gitspaceInstanceInsertColumns = `
gits_gitspace_config_id,
gits_url,
gits_state,
gits_user_uid,
gits_resource_usage,
gits_space_id,
gits_created,
gits_updated,
gits_last_used,
gits_total_time_used,
gits_tracked_changes,
gits_access_type,
gits_machine_user,
gits_uid,
gits_access_key_ref,
gits_last_heartbeat,
gits_active_time_started,
gits_active_time_ended`
gitspaceInstanceSelectColumns = "gits_id," + gitspaceInstanceInsertColumns
gitspaceInstanceTable = `gitspaces`
)
type gitspaceInstance struct {
ID int64 `db:"gits_id"`
GitSpaceConfigID int64 `db:"gits_gitspace_config_id"`
URL null.String `db:"gits_url"`
State enum.GitspaceInstanceStateType `db:"gits_state"`
// TODO: migrate to principal int64 id to use principal cache and consistent with Harness code.
UserUID string `db:"gits_user_uid"`
ResourceUsage null.String `db:"gits_resource_usage"`
SpaceID int64 `db:"gits_space_id"`
LastUsed null.Int `db:"gits_last_used"`
TotalTimeUsed int64 `db:"gits_total_time_used"`
TrackedChanges null.String `db:"gits_tracked_changes"`
AccessType enum.GitspaceAccessType `db:"gits_access_type"`
AccessKeyRef null.String `db:"gits_access_key_ref"`
MachineUser null.String `db:"gits_machine_user"`
Identifier string `db:"gits_uid"`
Created int64 `db:"gits_created"`
Updated int64 `db:"gits_updated"`
LastHeartbeat null.Int `db:"gits_last_heartbeat"`
ActiveTimeStarted null.Int `db:"gits_active_time_started"`
ActiveTimeEnded null.Int `db:"gits_active_time_ended"`
}
// NewGitspaceInstanceStore returns a new GitspaceInstanceStore.
func NewGitspaceInstanceStore(db *sqlx.DB) store.GitspaceInstanceStore {
return &gitspaceInstanceStore{
db: db,
}
}
type gitspaceInstanceStore struct {
db *sqlx.DB
}
func (g gitspaceInstanceStore) FindTotalUsage(
ctx context.Context,
fromTime int64,
toTime int64,
spaceIDs []int64,
) (int64, error) {
var greatest = "MAX"
var least = "MIN"
if g.db.DriverName() == "postgres" {
greatest = "GREATEST"
least = "LEAST"
}
innerQuery := squirrel.Select(
greatest+"(gits_active_time_started, ?) AS effective_start_time",
least+"(COALESCE(gits_active_time_ended, ?), ?) AS effective_end_time",
).
From(gitspaceInstanceTable).
Where(
squirrel.And{
squirrel.Lt{"gits_active_time_started": toTime},
squirrel.Or{
squirrel.Expr("gits_active_time_ended IS NULL"),
squirrel.Gt{"gits_active_time_ended": fromTime},
},
squirrel.Eq{"gits_space_id": spaceIDs},
},
)
innerQry, innerArgs, err := innerQuery.ToSql()
if err != nil {
return 0, err
}
query := squirrel.
Select("SUM(effective_end_time - effective_start_time) AS total_active_time").
From("(" + innerQry + ") AS subquery").PlaceholderFormat(squirrel.Dollar)
qry, _, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "Failed to convert squirrel builder to sql")
}
args := append([]any{fromTime, toTime, toTime}, innerArgs...)
var totalActiveTime sql.NullInt64
db := dbtx.GetAccessor(ctx, g.db)
err = db.GetContext(ctx, &totalActiveTime, qry, args...)
if err != nil {
return 0, err
}
if totalActiveTime.Valid {
return totalActiveTime.Int64, nil
}
return 0, nil
}
func (g gitspaceInstanceStore) Find(ctx context.Context, id int64) (*types.GitspaceInstance, error) {
stmt := database.Builder.
Select(gitspaceInstanceSelectColumns).
From(gitspaceInstanceTable).
Where("gits_id = ?", id)
sql, args, err := stmt.ToSql()
if err != nil {
return nil, errors.Wrap(err, "Failed to convert squirrel builder to sql")
}
gitspace := new(gitspaceInstance)
db := dbtx.GetAccessor(ctx, g.db)
if err := db.GetContext(ctx, gitspace, sql, args...); err != nil {
return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find gitspace %d", id)
}
return g.mapToGitspaceInstance(ctx, gitspace)
}
func (g gitspaceInstanceStore) FindByIdentifier(
ctx context.Context,
identifier string,
) (*types.GitspaceInstance, error) {
stmt := database.Builder.
Select(gitspaceInstanceSelectColumns).
From(gitspaceInstanceTable).
Where("gits_uid = ?", identifier)
sql, args, err := stmt.ToSql()
if err != nil {
return nil, errors.Wrap(err, "Failed to convert squirrel builder to sql")
}
gitspace := new(gitspaceInstance)
db := dbtx.GetAccessor(ctx, g.db)
if err := db.GetContext(ctx, gitspace, sql, args...); err != nil {
return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find gitspace %s", identifier)
}
return g.mapToGitspaceInstance(ctx, gitspace)
}
func (g gitspaceInstanceStore) Create(ctx context.Context, gitspaceInstance *types.GitspaceInstance) error {
stmt := database.Builder.
Insert(gitspaceInstanceTable).
Columns(gitspaceInstanceInsertColumns).
Values(
gitspaceInstance.GitSpaceConfigID,
gitspaceInstance.URL,
gitspaceInstance.State,
gitspaceInstance.UserID,
gitspaceInstance.ResourceUsage,
gitspaceInstance.SpaceID,
gitspaceInstance.Created,
gitspaceInstance.Updated,
gitspaceInstance.LastUsed,
gitspaceInstance.TotalTimeUsed,
gitspaceInstance.TrackedChanges,
gitspaceInstance.AccessType,
gitspaceInstance.MachineUser,
gitspaceInstance.Identifier,
gitspaceInstance.AccessKeyRef,
gitspaceInstance.LastHeartbeat,
gitspaceInstance.ActiveTimeStarted,
gitspaceInstance.ActiveTimeEnded,
).
Suffix(ReturningClause + "gits_id")
sql, args, err := stmt.ToSql()
if err != nil {
return errors.Wrap(err, "Failed to convert squirrel builder to sql")
}
db := dbtx.GetAccessor(ctx, g.db)
if err = db.QueryRowContext(ctx, sql, args...).Scan(&gitspaceInstance.ID); err != nil {
return database.ProcessSQLErrorf(
ctx, err, "gitspace instance query failed for %s", gitspaceInstance.Identifier)
}
return nil
}
func (g gitspaceInstanceStore) Update(
ctx context.Context,
gitspaceInstance *types.GitspaceInstance,
) error {
stmt := database.Builder.
Update(gitspaceInstanceTable).
Set("gits_state", gitspaceInstance.State).
Set("gits_last_used", gitspaceInstance.LastUsed).
Set("gits_last_heartbeat", gitspaceInstance.LastHeartbeat).
Set("gits_url", gitspaceInstance.URL).
Set("gits_active_time_started", gitspaceInstance.ActiveTimeStarted).
Set("gits_active_time_ended", gitspaceInstance.ActiveTimeEnded).
Set("gits_total_time_used", gitspaceInstance.TotalTimeUsed).
Set("gits_updated", gitspaceInstance.Updated).
Where("gits_id = ?", gitspaceInstance.ID)
sql, args, err := stmt.ToSql()
if err != nil {
return errors.Wrap(err, "Failed to convert squirrel builder to sql")
}
db := dbtx.GetAccessor(ctx, g.db)
if _, err := db.ExecContext(ctx, sql, args...); err != nil {
return database.ProcessSQLErrorf(
ctx, err, "Failed to update gitspace instance for %s", gitspaceInstance.Identifier)
}
return nil
}
func (g gitspaceInstanceStore) FindLatestByGitspaceConfigID(
ctx context.Context,
gitspaceConfigID int64,
) (*types.GitspaceInstance, error) {
stmt := database.Builder.
Select(gitspaceInstanceSelectColumns).
From(gitspaceInstanceTable).
Where("gits_gitspace_config_id = ?", gitspaceConfigID).
OrderBy("gits_created DESC")
sql, args, err := stmt.ToSql()
if err != nil {
return nil, errors.Wrap(err, "Failed to convert squirrel builder to sql")
}
gitspace := new(gitspaceInstance)
db := dbtx.GetAccessor(ctx, g.db)
if err := db.GetContext(ctx, gitspace, sql, args...); err != nil {
return nil, database.ProcessSQLErrorf(
ctx, err, "Failed to find latest gitspace instance for %d", gitspaceConfigID)
}
return g.mapToGitspaceInstance(ctx, gitspace)
}
func (g gitspaceInstanceStore) List(
ctx context.Context,
filter *types.GitspaceFilter,
) ([]*types.GitspaceInstance, error) {
stmt := database.Builder.
Select(gitspaceInstanceSelectColumns).
From(gitspaceInstanceTable).
Where(squirrel.Eq{"gits_space_id": filter.SpaceIDs}).
Where(squirrel.Eq{"gits_user_uid": filter.UserID}).
OrderBy("gits_created ASC")
sql, args, err := stmt.ToSql()
if err != nil {
return nil, errors.Wrap(err, "Failed to convert squirrel builder to sql")
}
db := dbtx.GetAccessor(ctx, g.db)
var dst []*gitspaceInstance
if err := db.SelectContext(ctx, &dst, sql, args...); err != nil {
return nil, database.ProcessSQLErrorf(ctx, err, "Failed executing gitspace instance list query")
}
return g.mapToGitspaceInstances(ctx, dst)
}
func (g gitspaceInstanceStore) ListInactive(
ctx context.Context,
filter *types.GitspaceFilter,
) ([]int64, error) {
stmt := database.Builder.
Select("gits_gitspace_config_id").
From(gitspaceInstanceTable).
Where(squirrel.Lt{"gits_last_used": filter.LastUsedBefore}).
Where(squirrel.Eq{"gits_state": filter.State}).
OrderBy("gits_created ASC")
sql, args, err := stmt.ToSql()
if err != nil {
return nil, errors.Wrap(err, "Failed to convert squirrel builder to sql")
}
db := dbtx.GetAccessor(ctx, g.db)
var dst []int64
if err := db.SelectContext(ctx, &dst, sql, args...); err != nil {
return nil, database.ProcessSQLErrorf(ctx, err, "Failed executing gitspace instance list query")
}
return dst, nil
}
func (g gitspaceInstanceStore) FindAllLatestByGitspaceConfigID(
ctx context.Context,
gitspaceConfigIDs []int64,
) ([]*types.GitspaceInstance, error) {
var whereClause = "(1=0)"
if len(gitspaceConfigIDs) > 0 {
whereClause = fmt.Sprintf("gits_gitspace_config_id IN (%s)",
strings.Trim(strings.Join(strings.Split(fmt.Sprint(gitspaceConfigIDs), " "), ","), "[]"))
}
baseSelect := squirrel.Select("*",
"ROW_NUMBER() OVER (PARTITION BY gits_gitspace_config_id "+
"ORDER BY gits_created DESC) AS rn").
From(gitspaceInstanceTable).
Where(whereClause)
// Use the base select query in a common table expression (CTE)
stmt := squirrel.Select(gitspaceInstanceSelectColumns).
FromSelect(baseSelect, "RankedRows").
Where("rn = 1")
sql, args, err := stmt.ToSql()
if err != nil {
return nil, errors.Wrap(err, "Failed to convert squirrel builder to sql")
}
db := dbtx.GetAccessor(ctx, g.db)
var dst []*gitspaceInstance
if err := db.SelectContext(ctx, &dst, sql, args...); err != nil {
return nil, database.ProcessSQLErrorf(
ctx, err, "Failed executing all latest gitspace instance list query")
}
return g.mapToGitspaceInstances(ctx, dst)
}
func (g gitspaceInstanceStore) mapToGitspaceInstance(
_ context.Context,
in *gitspaceInstance,
) (*types.GitspaceInstance, error) {
var res = &types.GitspaceInstance{
ID: in.ID,
Identifier: in.Identifier,
GitSpaceConfigID: in.GitSpaceConfigID,
URL: in.URL.Ptr(),
State: in.State,
UserID: in.UserUID,
ResourceUsage: in.ResourceUsage.Ptr(),
LastUsed: in.LastUsed.Ptr(),
TotalTimeUsed: in.TotalTimeUsed,
TrackedChanges: in.TrackedChanges.Ptr(),
AccessType: in.AccessType,
AccessKeyRef: in.AccessKeyRef.Ptr(),
MachineUser: in.MachineUser.Ptr(),
SpaceID: in.SpaceID,
Created: in.Created,
Updated: in.Updated,
LastHeartbeat: in.LastHeartbeat.Ptr(),
ActiveTimeEnded: in.ActiveTimeEnded.Ptr(),
ActiveTimeStarted: in.ActiveTimeStarted.Ptr(),
}
return res, nil
}
func (g gitspaceInstanceStore) mapToGitspaceInstances(
ctx context.Context,
instances []*gitspaceInstance,
) ([]*types.GitspaceInstance, error) {
var err error
res := make([]*types.GitspaceInstance, len(instances))
for i := range instances {
res[i], err = g.mapToGitspaceInstance(ctx, instances[i])
if err != nil {
return nil, err
}
}
return res, nil
}