concourse/atc/db/resource_config_scope.go

344 lines
8.1 KiB
Go

package db
import (
"database/sql"
"encoding/json"
"time"
"code.cloudfoundry.org/lager"
sq "github.com/Masterminds/squirrel"
"github.com/concourse/concourse/atc"
"github.com/concourse/concourse/atc/db/lock"
)
//go:generate counterfeiter . ResourceConfigScope
// ResourceConfigScope represents the relationship between a possible pipeline resource and a resource config.
// When a resource is specified to have a unique version history either through its base resource type or its custom
// resource type, it results in its generated resource config to be scoped to the resource. This relationship is
// translated into its row in the resource config scopes table to have both the resource id and resource config id
// populated. When a resource has a shared version history, its resource config is not scoped to the (or any) resource
// and its row in the resource config scopes table will have the resource config id populated but a NULL value for
// the resource id. Resource versions will therefore be directly dependent on a resource config scope.
type ResourceConfigScope interface {
ID() int
Resource() Resource
ResourceConfig() ResourceConfig
CheckError() error
SaveVersions(versions []atc.Version) error
FindVersion(atc.Version) (ResourceConfigVersion, bool, error)
LatestVersion() (ResourceConfigVersion, bool, error)
SetCheckError(error) error
AcquireResourceCheckingLock(
logger lager.Logger,
) (lock.Lock, bool, error)
UpdateLastCheckStartTime(
interval time.Duration,
immediate bool,
) (bool, error)
UpdateLastCheckEndTime() (bool, error)
}
type resourceConfigScope struct {
id int
resource Resource
resourceConfig ResourceConfig
checkError error
conn Conn
lockFactory lock.LockFactory
}
func (r *resourceConfigScope) ID() int { return r.id }
func (r *resourceConfigScope) Resource() Resource { return r.resource }
func (r *resourceConfigScope) ResourceConfig() ResourceConfig { return r.resourceConfig }
func (r *resourceConfigScope) CheckError() error { return r.checkError }
// SaveVersions stores a list of version in the db for a resource config
// Each version will also have its check order field updated and the
// Cache index for pipelines using the resource config will be bumped.
//
// In the case of a check resource from an older version, the versions
// that already exist in the DB will be re-ordered using
// incrementCheckOrderWhenNewerVersion to input the correct check order
func (r *resourceConfigScope) SaveVersions(versions []atc.Version) error {
tx, err := r.conn.Begin()
if err != nil {
return err
}
defer Rollback(tx)
for _, version := range versions {
_, err = saveResourceVersion(tx, r, version, nil)
if err != nil {
return err
}
versionJSON, err := json.Marshal(version)
if err != nil {
return err
}
err = incrementCheckOrder(tx, r, string(versionJSON))
if err != nil {
return err
}
}
err = tx.Commit()
if err != nil {
return err
}
err = bumpCacheIndexForPipelinesUsingResourceConfigScope(r.conn, r.id)
if err != nil {
return err
}
return nil
}
func (r *resourceConfigScope) FindVersion(v atc.Version) (ResourceConfigVersion, bool, error) {
rcv := &resourceConfigVersion{
resourceConfigScope: r,
conn: r.conn,
}
versionByte, err := json.Marshal(v)
if err != nil {
return nil, false, err
}
row := resourceConfigVersionQuery.
Where(sq.Eq{
"v.resource_config_scope_id": r.id,
}).
Where(sq.Expr("v.version_md5 = md5(?)", versionByte)).
RunWith(r.conn).
QueryRow()
err = scanResourceConfigVersion(rcv, row)
if err != nil {
if err == sql.ErrNoRows {
return nil, false, nil
}
return nil, false, err
}
return rcv, true, nil
}
func (r *resourceConfigScope) LatestVersion() (ResourceConfigVersion, bool, error) {
rcv := &resourceConfigVersion{
conn: r.conn,
resourceConfigScope: r,
}
row := resourceConfigVersionQuery.
Where(sq.Eq{"v.resource_config_scope_id": r.id}).
OrderBy("v.check_order DESC").
Limit(1).
RunWith(r.conn).
QueryRow()
err := scanResourceConfigVersion(rcv, row)
if err != nil {
if err == sql.ErrNoRows {
return nil, false, nil
}
return nil, false, err
}
return rcv, true, nil
}
func (r *resourceConfigScope) SetCheckError(cause error) error {
var err error
if cause == nil {
_, err = psql.Update("resource_config_scopes").
Set("check_error", nil).
Where(sq.Eq{"id": r.id}).
RunWith(r.conn).
Exec()
} else {
_, err = psql.Update("resource_config_scopes").
Set("check_error", cause.Error()).
Where(sq.Eq{"id": r.id}).
RunWith(r.conn).
Exec()
}
return err
}
func (r *resourceConfigScope) AcquireResourceCheckingLock(
logger lager.Logger,
) (lock.Lock, bool, error) {
return r.lockFactory.Acquire(
logger,
lock.NewResourceConfigCheckingLockID(r.resourceConfig.ID()),
)
}
func (r *resourceConfigScope) UpdateLastCheckStartTime(
interval time.Duration,
immediate bool,
) (bool, error) {
tx, err := r.conn.Begin()
if err != nil {
return false, err
}
defer Rollback(tx)
params := []interface{}{r.id}
condition := ""
if !immediate {
condition = "AND now() - last_check_start_time > ($2 || ' SECONDS')::INTERVAL"
params = append(params, interval.Seconds())
}
updated, err := checkIfRowsUpdated(tx, `
UPDATE resource_config_scopes
SET last_check_start_time = now()
WHERE id = $1
`+condition, params...)
if err != nil {
return false, err
}
if !updated {
return false, nil
}
err = tx.Commit()
if err != nil {
return false, err
}
return true, nil
}
func (r *resourceConfigScope) UpdateLastCheckEndTime() (bool, error) {
tx, err := r.conn.Begin()
if err != nil {
return false, err
}
defer Rollback(tx)
updated, err := checkIfRowsUpdated(tx, `
UPDATE resource_config_scopes
SET last_check_end_time = now()
WHERE id = $1
`, r.id)
if err != nil {
return false, err
}
if !updated {
return false, nil
}
err = tx.Commit()
if err != nil {
return false, err
}
return true, nil
}
func saveResourceVersion(tx Tx, r ResourceConfigScope, version atc.Version, metadata ResourceConfigMetadataFields) (bool, error) {
versionJSON, err := json.Marshal(version)
if err != nil {
return false, err
}
metadataJSON, err := json.Marshal(metadata)
if err != nil {
return false, err
}
var checkOrder int
err = tx.QueryRow(`
INSERT INTO resource_config_versions (resource_config_scope_id, version, version_md5, metadata)
SELECT $1, $2, md5($3), $4
ON CONFLICT (resource_config_scope_id, version_md5) DO UPDATE SET metadata = $4
RETURNING check_order
`, r.ID(), string(versionJSON), string(versionJSON), string(metadataJSON)).Scan(&checkOrder)
if err != nil {
return false, err
}
return checkOrder == 0, nil
}
// increment the check order if the version's check order is less than the
// current max. This will fix the case of a check from an old version causing
// the desired order to change; existing versions will be re-ordered since
// we add them in the desired order.
func incrementCheckOrder(tx Tx, r ResourceConfigScope, version string) error {
_, err := tx.Exec(`
WITH max_checkorder AS (
SELECT max(check_order) co
FROM resource_config_versions
WHERE resource_config_scope_id = $1
)
UPDATE resource_config_versions
SET check_order = mc.co + 1
FROM max_checkorder mc
WHERE resource_config_scope_id = $1
AND version = $2
AND check_order <= mc.co;`, r.ID(), version)
return err
}
func bumpCacheIndexForPipelinesUsingResourceConfigScope(conn Conn, rcsID int) error {
rows, err := psql.Select("p.id").
From("pipelines p").
Join("resources r ON r.pipeline_id = p.id").
Where(sq.Eq{
"r.resource_config_scope_id": rcsID,
}).
RunWith(conn).
Query()
if err != nil {
return err
}
var pipelines []int
for rows.Next() {
var pid int
err = rows.Scan(&pid)
if err != nil {
return err
}
pipelines = append(pipelines, pid)
}
for _, p := range pipelines {
_, err := psql.Update("pipelines").
Set("cache_index", sq.Expr("cache_index + 1")).
Where(sq.Eq{
"id": p,
}).
RunWith(conn).
Exec()
if err != nil {
return err
}
}
return nil
}