mirror of https://github.com/harness/drone.git
584 lines
17 KiB
Go
584 lines
17 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"
|
|
"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"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
var _ store.ConnectorStore = (*connectorStore)(nil)
|
|
|
|
const (
|
|
//nolint:goconst
|
|
connectorQueryBase = `
|
|
SELECT` + connectorColumns + `
|
|
FROM connectors`
|
|
|
|
connectorColumns = `
|
|
connector_id,
|
|
connector_identifier,
|
|
connector_description,
|
|
connector_type,
|
|
connector_auth_type,
|
|
connector_created_by,
|
|
connector_space_id,
|
|
connector_last_test_attempt,
|
|
connector_last_test_error_msg,
|
|
connector_last_test_status,
|
|
connector_created,
|
|
connector_updated,
|
|
connector_version,
|
|
connector_address,
|
|
connector_insecure,
|
|
connector_username,
|
|
connector_github_app_installation_id,
|
|
connector_github_app_application_id,
|
|
connector_region,
|
|
connector_password,
|
|
connector_token,
|
|
connector_aws_key,
|
|
connector_aws_secret,
|
|
connector_github_app_private_key,
|
|
connector_token_refresh
|
|
`
|
|
)
|
|
|
|
type connector struct {
|
|
ID int64 `db:"connector_id"`
|
|
Identifier string `db:"connector_identifier"`
|
|
Description string `db:"connector_description"`
|
|
Type string `db:"connector_type"`
|
|
AuthType string `db:"connector_auth_type"`
|
|
CreatedBy int64 `db:"connector_created_by"`
|
|
SpaceID int64 `db:"connector_space_id"`
|
|
LastTestAttempt int64 `db:"connector_last_test_attempt"`
|
|
LastTestErrorMsg string `db:"connector_last_test_error_msg"`
|
|
LastTestStatus string `db:"connector_last_test_status"`
|
|
Created int64 `db:"connector_created"`
|
|
Updated int64 `db:"connector_updated"`
|
|
Version int64 `db:"connector_version"`
|
|
|
|
Address sql.NullString `db:"connector_address"`
|
|
Insecure sql.NullBool `db:"connector_insecure"`
|
|
Username sql.NullString `db:"connector_username"`
|
|
GithubAppInstallationID sql.NullString `db:"connector_github_app_installation_id"`
|
|
GithubAppApplicationID sql.NullString `db:"connector_github_app_application_id"`
|
|
Region sql.NullString `db:"connector_region"`
|
|
// Password fields are stored as reference to secrets table
|
|
Password sql.NullInt64 `db:"connector_password"`
|
|
Token sql.NullInt64 `db:"connector_token"`
|
|
AWSKey sql.NullInt64 `db:"connector_aws_key"`
|
|
AWSSecret sql.NullInt64 `db:"connector_aws_secret"`
|
|
GithubAppPrivateKey sql.NullInt64 `db:"connector_github_app_private_key"`
|
|
TokenRefresh sql.NullInt64 `db:"connector_token_refresh"`
|
|
}
|
|
|
|
// NewConnectorStore returns a new ConnectorStore.
|
|
// The secret store is used to resolve the secret references.
|
|
func NewConnectorStore(db *sqlx.DB, secretStore store.SecretStore) store.ConnectorStore {
|
|
return &connectorStore{
|
|
db: db,
|
|
secretStore: secretStore,
|
|
}
|
|
}
|
|
|
|
func (s *connectorStore) mapFromDBConnectors(ctx context.Context, src []*connector) ([]*types.Connector, error) {
|
|
dst := make([]*types.Connector, len(src))
|
|
for i, v := range src {
|
|
m, err := s.mapFromDBConnector(ctx, v)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not map from db connector: %w", err)
|
|
}
|
|
dst[i] = m
|
|
}
|
|
return dst, nil
|
|
}
|
|
|
|
func (s *connectorStore) mapToDBConnector(ctx context.Context, v *types.Connector) (*connector, error) {
|
|
to := connector{
|
|
ID: v.ID,
|
|
Identifier: v.Identifier,
|
|
Description: v.Description,
|
|
Type: v.Type.String(),
|
|
SpaceID: v.SpaceID,
|
|
CreatedBy: v.CreatedBy,
|
|
Created: v.Created,
|
|
Updated: v.Updated,
|
|
Version: v.Version,
|
|
LastTestAttempt: v.LastTestAttempt,
|
|
LastTestErrorMsg: v.LastTestErrorMsg,
|
|
LastTestStatus: v.LastTestStatus.String(),
|
|
}
|
|
// Parse connector specific configs
|
|
err := s.convertConfigToDB(ctx, v, &to)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not convert config to db: %w", err)
|
|
}
|
|
return &to, nil
|
|
}
|
|
|
|
func (s *connectorStore) convertConfigToDB(
|
|
ctx context.Context,
|
|
source *types.Connector,
|
|
to *connector,
|
|
) error {
|
|
switch {
|
|
case source.Github != nil:
|
|
to.Address = sql.NullString{String: source.Github.APIURL, Valid: true}
|
|
to.Insecure = sql.NullBool{Bool: source.Github.Insecure, Valid: true}
|
|
if source.Github.Auth == nil {
|
|
return fmt.Errorf("auth is required for github connectors")
|
|
}
|
|
if source.Github.Auth.AuthType != enum.ConnectorAuthTypeBearer {
|
|
return fmt.Errorf("only bearer token auth is supported for github connectors")
|
|
}
|
|
to.AuthType = source.Github.Auth.AuthType.String()
|
|
creds := source.Github.Auth.Bearer
|
|
// use the same space ID as the connector
|
|
tokenID, err := s.secretIdentiferToID(ctx, creds.Token.Identifier, source.SpaceID)
|
|
if err != nil {
|
|
return fmt.Errorf("could not find secret: %w", err)
|
|
}
|
|
to.Token = sql.NullInt64{Int64: tokenID, Valid: true}
|
|
default:
|
|
return fmt.Errorf("no connector config found for type: %s", source.Type)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// secretIdentiferToID finds the secret ID given the space ID and the identifier.
|
|
func (s *connectorStore) secretIdentiferToID(
|
|
ctx context.Context,
|
|
identifier string,
|
|
spaceID int64,
|
|
) (int64, error) {
|
|
secret, err := s.secretStore.FindByIdentifier(ctx, spaceID, identifier)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return secret.ID, nil
|
|
}
|
|
|
|
func (s *connectorStore) mapFromDBConnector(
|
|
ctx context.Context,
|
|
dbConnector *connector,
|
|
) (*types.Connector, error) {
|
|
connector := &types.Connector{
|
|
ID: dbConnector.ID,
|
|
Identifier: dbConnector.Identifier,
|
|
Description: dbConnector.Description,
|
|
Type: enum.ConnectorType(dbConnector.Type),
|
|
SpaceID: dbConnector.SpaceID,
|
|
CreatedBy: dbConnector.CreatedBy,
|
|
LastTestAttempt: dbConnector.LastTestAttempt,
|
|
LastTestErrorMsg: dbConnector.LastTestErrorMsg,
|
|
LastTestStatus: enum.ConnectorStatus(dbConnector.LastTestStatus),
|
|
Created: dbConnector.Created,
|
|
Updated: dbConnector.Updated,
|
|
Version: dbConnector.Version,
|
|
}
|
|
err := s.populateConnectorData(ctx, dbConnector, connector)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not populate connector data: %w", err)
|
|
}
|
|
return connector, nil
|
|
}
|
|
|
|
func (s *connectorStore) populateConnectorData(
|
|
ctx context.Context,
|
|
source *connector,
|
|
to *types.Connector,
|
|
) error {
|
|
switch enum.ConnectorType(source.Type) {
|
|
case enum.ConnectorTypeGithub:
|
|
githubData, err := s.parseGithubConnectorData(ctx, source)
|
|
if err != nil {
|
|
return fmt.Errorf("could not parse github connector data: %w", err)
|
|
}
|
|
to.Github = githubData
|
|
// Cases for other connectors can be added here
|
|
default:
|
|
return fmt.Errorf("unsupported connector type: %s", source.Type)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *connectorStore) parseGithubConnectorData(
|
|
ctx context.Context,
|
|
connector *connector,
|
|
) (*types.GithubConnectorData, error) {
|
|
auth, err := s.parseAuthenticationData(ctx, connector)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not parse authentication data: %w", err)
|
|
}
|
|
return &types.GithubConnectorData{
|
|
APIURL: connector.Address.String,
|
|
Insecure: connector.Insecure.Bool,
|
|
Auth: auth,
|
|
}, nil
|
|
}
|
|
|
|
func (s *connectorStore) parseAuthenticationData(
|
|
ctx context.Context,
|
|
connector *connector,
|
|
) (*types.ConnectorAuth, error) {
|
|
authType, err := enum.ParseConnectorAuthType(connector.AuthType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch authType {
|
|
case enum.ConnectorAuthTypeBasic:
|
|
if !connector.Username.Valid || !connector.Password.Valid {
|
|
return nil, fmt.Errorf("basic auth requires both username and password")
|
|
}
|
|
passwordRef, err := s.convertToRef(ctx, connector.Password.Int64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not convert basicauth password to ref: %w", err)
|
|
}
|
|
return &types.ConnectorAuth{
|
|
AuthType: enum.ConnectorAuthTypeBasic,
|
|
Basic: &types.BasicAuthCreds{
|
|
Username: connector.Username.String,
|
|
Password: passwordRef,
|
|
},
|
|
}, nil
|
|
case enum.ConnectorAuthTypeBearer:
|
|
if !connector.Token.Valid {
|
|
return nil, fmt.Errorf("bearer auth requires a token")
|
|
}
|
|
tokenRef, err := s.convertToRef(ctx, connector.Token.Int64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not convert bearer token to ref: %w", err)
|
|
}
|
|
return &types.ConnectorAuth{
|
|
AuthType: enum.ConnectorAuthTypeBearer,
|
|
Bearer: &types.BearerTokenCreds{
|
|
Token: tokenRef,
|
|
},
|
|
}, nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported auth type: %s", connector.AuthType)
|
|
}
|
|
}
|
|
|
|
func (s *connectorStore) convertToRef(ctx context.Context, id int64) (types.SecretRef, error) {
|
|
secret, err := s.secretStore.Find(ctx, id)
|
|
if err != nil {
|
|
return types.SecretRef{}, err
|
|
}
|
|
return types.SecretRef{
|
|
Identifier: secret.Identifier,
|
|
}, nil
|
|
}
|
|
|
|
type connectorStore struct {
|
|
db *sqlx.DB
|
|
secretStore store.SecretStore
|
|
}
|
|
|
|
// Find returns a connector given a connector ID.
|
|
func (s *connectorStore) Find(ctx context.Context, id int64) (*types.Connector, error) {
|
|
const findQueryStmt = connectorQueryBase + `
|
|
WHERE connector_id = $1`
|
|
db := dbtx.GetAccessor(ctx, s.db)
|
|
|
|
dst := new(connector)
|
|
if err := db.GetContext(ctx, dst, findQueryStmt, id); err != nil {
|
|
return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find connector")
|
|
}
|
|
return s.mapFromDBConnector(ctx, dst)
|
|
}
|
|
|
|
// FindByIdentifier returns a connector in a given space with a given identifier.
|
|
func (s *connectorStore) FindByIdentifier(
|
|
ctx context.Context,
|
|
spaceID int64,
|
|
identifier string,
|
|
) (*types.Connector, error) {
|
|
const findQueryStmt = connectorQueryBase + `
|
|
WHERE connector_space_id = $1 AND connector_identifier = $2`
|
|
db := dbtx.GetAccessor(ctx, s.db)
|
|
|
|
dst := new(connector)
|
|
if err := db.GetContext(ctx, dst, findQueryStmt, spaceID, identifier); err != nil {
|
|
return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find connector")
|
|
}
|
|
return s.mapFromDBConnector(ctx, dst)
|
|
}
|
|
|
|
// Create creates a connector.
|
|
func (s *connectorStore) Create(ctx context.Context, connector *types.Connector) error {
|
|
dbConnector, err := s.mapToDBConnector(ctx, connector)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
const connectorInsertStmt = `
|
|
INSERT INTO connectors (
|
|
connector_description
|
|
,connector_type
|
|
,connector_created_by
|
|
,connector_space_id
|
|
,connector_identifier
|
|
,connector_last_test_attempt
|
|
,connector_last_test_error_msg
|
|
,connector_last_test_status
|
|
,connector_created
|
|
,connector_updated
|
|
,connector_version
|
|
,connector_auth_type
|
|
,connector_address
|
|
,connector_insecure
|
|
,connector_username
|
|
,connector_github_app_installation_id
|
|
,connector_github_app_application_id
|
|
,connector_region
|
|
,connector_password
|
|
,connector_token
|
|
,connector_aws_key
|
|
,connector_aws_secret
|
|
,connector_github_app_private_key
|
|
,connector_token_refresh
|
|
) VALUES (
|
|
:connector_description
|
|
,:connector_type
|
|
,:connector_created_by
|
|
,:connector_space_id
|
|
,:connector_identifier
|
|
,:connector_last_test_attempt
|
|
,:connector_last_test_error_msg
|
|
,:connector_last_test_status
|
|
,:connector_created
|
|
,:connector_updated
|
|
,:connector_version
|
|
,:connector_auth_type
|
|
,:connector_address
|
|
,:connector_insecure
|
|
,:connector_username
|
|
,:connector_github_app_installation_id
|
|
,:connector_github_app_application_id
|
|
,:connector_region
|
|
,:connector_password
|
|
,:connector_token
|
|
,:connector_aws_key
|
|
,:connector_aws_secret
|
|
,:connector_github_app_private_key
|
|
,:connector_token_refresh
|
|
) RETURNING connector_id`
|
|
db := dbtx.GetAccessor(ctx, s.db)
|
|
|
|
query, arg, err := db.BindNamed(connectorInsertStmt, dbConnector)
|
|
if err != nil {
|
|
return database.ProcessSQLErrorf(ctx, err, "Failed to bind connector object")
|
|
}
|
|
|
|
if err = db.QueryRowContext(ctx, query, arg...).Scan(&connector.ID); err != nil {
|
|
return database.ProcessSQLErrorf(ctx, err, "connector query failed")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *connectorStore) Update(ctx context.Context, p *types.Connector) error {
|
|
conn, err := s.mapToDBConnector(ctx, p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
const connectorUpdateStmt = `
|
|
UPDATE connectors
|
|
SET
|
|
connector_description = :connector_description
|
|
,connector_identifier = :connector_identifier
|
|
,connector_last_test_attempt = :connector_last_test_attempt
|
|
,connector_last_test_error_msg = :connector_last_test_error_msg
|
|
,connector_last_test_status = :connector_last_test_status
|
|
,connector_updated = :connector_updated
|
|
,connector_version = :connector_version
|
|
,connector_auth_type = :connector_auth_type
|
|
,connector_address = :connector_address
|
|
,connector_insecure = :connector_insecure
|
|
,connector_username = :connector_username
|
|
,connector_github_app_installation_id = :connector_github_app_installation_id
|
|
,connector_github_app_application_id = :connector_github_app_application_id
|
|
,connector_region = :connector_region
|
|
,connector_password = :connector_password
|
|
,connector_token = :connector_token
|
|
,connector_aws_key = :connector_aws_key
|
|
,connector_aws_secret = :connector_aws_secret
|
|
,connector_github_app_private_key = :connector_github_app_private_key
|
|
,connector_token_refresh = :connector_token_refresh
|
|
WHERE connector_id = :connector_id AND connector_version = :connector_version - 1`
|
|
o := *conn
|
|
|
|
o.Version++
|
|
o.Updated = time.Now().UnixMilli()
|
|
|
|
db := dbtx.GetAccessor(ctx, s.db)
|
|
|
|
query, arg, err := db.BindNamed(connectorUpdateStmt, o)
|
|
if err != nil {
|
|
return database.ProcessSQLErrorf(ctx, err, "Failed to bind connector object")
|
|
}
|
|
|
|
result, err := db.ExecContext(ctx, query, arg...)
|
|
if err != nil {
|
|
return database.ProcessSQLErrorf(ctx, err, "Failed to update connector")
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
p.Version = o.Version
|
|
p.Updated = o.Updated
|
|
return nil
|
|
}
|
|
|
|
// UpdateOptLock updates the connector using the optimistic locking mechanism.
|
|
func (s *connectorStore) UpdateOptLock(ctx context.Context,
|
|
connector *types.Connector,
|
|
mutateFn func(connector *types.Connector) error,
|
|
) (*types.Connector, error) {
|
|
for {
|
|
dup := *connector
|
|
|
|
err := mutateFn(&dup)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = s.Update(ctx, &dup)
|
|
if err == nil {
|
|
return &dup, nil
|
|
}
|
|
if !errors.Is(err, gitness_store.ErrVersionConflict) {
|
|
return nil, err
|
|
}
|
|
|
|
connector, err = s.Find(ctx, connector.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// List lists all the connectors present in a space.
|
|
func (s *connectorStore) List(
|
|
ctx context.Context,
|
|
parentID int64,
|
|
filter types.ListQueryFilter,
|
|
) ([]*types.Connector, error) {
|
|
stmt := database.Builder.
|
|
Select(connectorColumns).
|
|
From("connectors").
|
|
Where("connector_space_id = ?", fmt.Sprint(parentID))
|
|
|
|
if filter.Query != "" {
|
|
stmt = stmt.Where("LOWER(connector_identifier) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(filter.Query)))
|
|
}
|
|
|
|
stmt = stmt.Limit(database.Limit(filter.Size))
|
|
stmt = stmt.Offset(database.Offset(filter.Page, filter.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 := []*connector{}
|
|
if err = db.SelectContext(ctx, &dst, sql, args...); err != nil {
|
|
return nil, database.ProcessSQLErrorf(ctx, err, "Failed executing custom list query")
|
|
}
|
|
|
|
return s.mapFromDBConnectors(ctx, dst)
|
|
}
|
|
|
|
// Delete deletes a connector given a connector ID.
|
|
func (s *connectorStore) Delete(ctx context.Context, id int64) error {
|
|
const connectorDeleteStmt = `
|
|
DELETE FROM connectors
|
|
WHERE connector_id = $1`
|
|
|
|
db := dbtx.GetAccessor(ctx, s.db)
|
|
|
|
if _, err := db.ExecContext(ctx, connectorDeleteStmt, id); err != nil {
|
|
return database.ProcessSQLErrorf(ctx, err, "Could not delete connector")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DeleteByIdentifier deletes a connector with a given identifier in a space.
|
|
func (s *connectorStore) DeleteByIdentifier(ctx context.Context, spaceID int64, identifier string) error {
|
|
const connectorDeleteStmt = `
|
|
DELETE FROM connectors
|
|
WHERE connector_space_id = $1 AND connector_identifier = $2`
|
|
|
|
db := dbtx.GetAccessor(ctx, s.db)
|
|
|
|
if _, err := db.ExecContext(ctx, connectorDeleteStmt, spaceID, identifier); err != nil {
|
|
return database.ProcessSQLErrorf(ctx, err, "Could not delete connector")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Count of connectors in a space.
|
|
func (s *connectorStore) Count(ctx context.Context, parentID int64, filter types.ListQueryFilter) (int64, error) {
|
|
stmt := database.Builder.
|
|
Select("count(*)").
|
|
From("connectors").
|
|
Where("connector_space_id = ?", parentID)
|
|
|
|
if filter.Query != "" {
|
|
stmt = stmt.Where("LOWER(connector_identifier) LIKE ?", fmt.Sprintf("%%%s%%", filter.Query))
|
|
}
|
|
|
|
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
|
|
}
|