// 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 }