1304 lines
30 KiB
Go
1304 lines
30 KiB
Go
package db
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
"code.cloudfoundry.org/lager"
|
|
sq "github.com/Masterminds/squirrel"
|
|
"github.com/concourse/concourse/atc"
|
|
"github.com/concourse/concourse/atc/db/encryption"
|
|
"github.com/concourse/concourse/atc/db/lock"
|
|
"github.com/concourse/concourse/atc/event"
|
|
"github.com/lib/pq"
|
|
)
|
|
|
|
const schema = "exec.v2"
|
|
|
|
type BuildInput struct {
|
|
Name string
|
|
Version atc.Version
|
|
ResourceID int
|
|
|
|
FirstOccurrence bool
|
|
}
|
|
|
|
type BuildOutput struct {
|
|
Name string
|
|
Version atc.Version
|
|
}
|
|
|
|
type BuildStatus string
|
|
|
|
const (
|
|
BuildStatusPending BuildStatus = "pending"
|
|
BuildStatusStarted BuildStatus = "started"
|
|
BuildStatusAborted BuildStatus = "aborted"
|
|
BuildStatusSucceeded BuildStatus = "succeeded"
|
|
BuildStatusFailed BuildStatus = "failed"
|
|
BuildStatusErrored BuildStatus = "errored"
|
|
)
|
|
|
|
var buildsQuery = psql.Select("b.id, b.name, b.job_id, b.team_id, b.status, b.manually_triggered, b.scheduled, b.schema, b.private_plan, b.public_plan, b.create_time, b.start_time, b.end_time, b.reap_time, j.name, b.pipeline_id, p.name, t.name, b.nonce, b.drained, b.aborted, b.completed").
|
|
From("builds b").
|
|
JoinClause("LEFT OUTER JOIN jobs j ON b.job_id = j.id").
|
|
JoinClause("LEFT OUTER JOIN pipelines p ON b.pipeline_id = p.id").
|
|
JoinClause("LEFT OUTER JOIN teams t ON b.team_id = t.id")
|
|
|
|
var minMaxIdQuery = psql.Select("COALESCE(MAX(b.id), 0)", "COALESCE(MIN(b.id), 0)").
|
|
From("builds as b")
|
|
|
|
//go:generate counterfeiter . Build
|
|
|
|
type Build interface {
|
|
ID() int
|
|
Name() string
|
|
JobID() int
|
|
JobName() string
|
|
PipelineID() int
|
|
PipelineName() string
|
|
TeamID() int
|
|
TeamName() string
|
|
Schema() string
|
|
PrivatePlan() atc.Plan
|
|
PublicPlan() *json.RawMessage
|
|
HasPlan() bool
|
|
Status() BuildStatus
|
|
StartTime() time.Time
|
|
CreateTime() time.Time
|
|
EndTime() time.Time
|
|
ReapTime() time.Time
|
|
IsManuallyTriggered() bool
|
|
IsScheduled() bool
|
|
IsRunning() bool
|
|
IsCompleted() bool
|
|
|
|
Reload() (bool, error)
|
|
|
|
AcquireTrackingLock(logger lager.Logger, interval time.Duration) (lock.Lock, bool, error)
|
|
|
|
Interceptible() (bool, error)
|
|
Preparation() (BuildPreparation, bool, error)
|
|
|
|
Start(atc.Plan) (bool, error)
|
|
Finish(BuildStatus) error
|
|
|
|
SetInterceptible(bool) error
|
|
|
|
Events(uint) (EventSource, error)
|
|
SaveEvent(event atc.Event) error
|
|
|
|
Artifacts() ([]WorkerArtifact, error)
|
|
Artifact(artifactID int) (WorkerArtifact, error)
|
|
|
|
SaveOutput(string, atc.Source, atc.VersionedResourceTypes, atc.Version, ResourceConfigMetadataFields, string, string) error
|
|
UseInputs(inputs []BuildInput) error
|
|
|
|
Resources() ([]BuildInput, []BuildOutput, error)
|
|
SaveImageResourceVersion(UsedResourceCache) error
|
|
|
|
Pipeline() (Pipeline, bool, error)
|
|
|
|
Delete() (bool, error)
|
|
MarkAsAborted() error
|
|
IsAborted() bool
|
|
AbortNotifier() (Notifier, error)
|
|
Schedule() (bool, error)
|
|
|
|
IsDrained() bool
|
|
SetDrained(bool) error
|
|
}
|
|
|
|
type build struct {
|
|
id int
|
|
name string
|
|
status BuildStatus
|
|
scheduled bool
|
|
|
|
teamID int
|
|
teamName string
|
|
|
|
pipelineID int
|
|
pipelineName string
|
|
jobID int
|
|
jobName string
|
|
|
|
isManuallyTriggered bool
|
|
|
|
schema string
|
|
privatePlan atc.Plan
|
|
publicPlan *json.RawMessage
|
|
|
|
createTime time.Time
|
|
startTime time.Time
|
|
endTime time.Time
|
|
reapTime time.Time
|
|
|
|
conn Conn
|
|
lockFactory lock.LockFactory
|
|
drained bool
|
|
aborted bool
|
|
completed bool
|
|
}
|
|
|
|
var ErrBuildDisappeared = errors.New("build disappeared from db")
|
|
var ErrBuildHasNoPipeline = errors.New("build has no pipeline")
|
|
var ErrBuildArtifactNotFound = errors.New("build artifact not found")
|
|
|
|
type ResourceNotFoundInPipeline struct {
|
|
Resource string
|
|
Pipeline string
|
|
}
|
|
|
|
func (r ResourceNotFoundInPipeline) Error() string {
|
|
return fmt.Sprintf("resource %s not found in pipeline %s", r.Resource, r.Pipeline)
|
|
}
|
|
|
|
func (b *build) ID() int { return b.id }
|
|
func (b *build) Name() string { return b.name }
|
|
func (b *build) JobID() int { return b.jobID }
|
|
func (b *build) JobName() string { return b.jobName }
|
|
func (b *build) PipelineID() int { return b.pipelineID }
|
|
func (b *build) PipelineName() string { return b.pipelineName }
|
|
func (b *build) TeamID() int { return b.teamID }
|
|
func (b *build) TeamName() string { return b.teamName }
|
|
func (b *build) IsManuallyTriggered() bool { return b.isManuallyTriggered }
|
|
func (b *build) Schema() string { return b.schema }
|
|
func (b *build) PrivatePlan() atc.Plan { return b.privatePlan }
|
|
func (b *build) PublicPlan() *json.RawMessage { return b.publicPlan }
|
|
func (b *build) HasPlan() bool { return string(*b.publicPlan) != "{}" }
|
|
func (b *build) CreateTime() time.Time { return b.createTime }
|
|
func (b *build) StartTime() time.Time { return b.startTime }
|
|
func (b *build) EndTime() time.Time { return b.endTime }
|
|
func (b *build) ReapTime() time.Time { return b.reapTime }
|
|
func (b *build) Status() BuildStatus { return b.status }
|
|
func (b *build) IsScheduled() bool { return b.scheduled }
|
|
func (b *build) IsDrained() bool { return b.drained }
|
|
func (b *build) IsRunning() bool { return !b.completed }
|
|
func (b *build) IsAborted() bool { return b.aborted }
|
|
func (b *build) IsCompleted() bool { return b.completed }
|
|
|
|
func (b *build) Reload() (bool, error) {
|
|
row := buildsQuery.Where(sq.Eq{"b.id": b.id}).
|
|
RunWith(b.conn).
|
|
QueryRow()
|
|
|
|
err := scanBuild(b, row, b.conn.EncryptionStrategy())
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (b *build) Interceptible() (bool, error) {
|
|
var interceptible bool
|
|
|
|
err := psql.Select("interceptible").
|
|
From("builds").
|
|
Where(sq.Eq{
|
|
"id": b.id,
|
|
}).
|
|
RunWith(b.conn).
|
|
QueryRow().Scan(&interceptible)
|
|
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
|
|
return interceptible, nil
|
|
}
|
|
|
|
func (b *build) SetInterceptible(i bool) error {
|
|
rows, err := psql.Update("builds").
|
|
Set("interceptible", i).
|
|
Where(sq.Eq{
|
|
"id": b.id,
|
|
}).
|
|
RunWith(b.conn).
|
|
Exec()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
affected, err := rows.RowsAffected()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if affected == 0 {
|
|
return ErrBuildDisappeared
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *build) Start(plan atc.Plan) (bool, error) {
|
|
tx, err := b.conn.Begin()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
defer Rollback(tx)
|
|
|
|
metadata, err := json.Marshal(plan)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
encryptedPlan, nonce, err := b.conn.EncryptionStrategy().Encrypt([]byte(metadata))
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
var startTime time.Time
|
|
|
|
err = psql.Update("builds").
|
|
Set("status", BuildStatusStarted).
|
|
Set("start_time", sq.Expr("now()")).
|
|
Set("schema", schema).
|
|
Set("private_plan", encryptedPlan).
|
|
Set("public_plan", plan.Public()).
|
|
Set("nonce", nonce).
|
|
Where(sq.Eq{
|
|
"id": b.id,
|
|
"status": "pending",
|
|
"aborted": false,
|
|
}).
|
|
Suffix("RETURNING start_time").
|
|
RunWith(tx).
|
|
QueryRow().
|
|
Scan(&startTime)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
err = b.saveEvent(tx, event.Status{
|
|
Status: atc.StatusStarted,
|
|
Time: startTime.Unix(),
|
|
})
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if b.jobID != 0 {
|
|
err = updateNextBuildForJob(tx, b.jobID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
err = tx.Commit()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
err = b.conn.Bus().Notify(buildEventsChannel(b.id))
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (b *build) Finish(status BuildStatus) error {
|
|
tx, err := b.conn.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer Rollback(tx)
|
|
|
|
var endTime time.Time
|
|
|
|
err = psql.Update("builds").
|
|
Set("status", status).
|
|
Set("end_time", sq.Expr("now()")).
|
|
Set("completed", true).
|
|
Set("private_plan", nil).
|
|
Set("nonce", nil).
|
|
Where(sq.Eq{"id": b.id}).
|
|
Suffix("RETURNING end_time").
|
|
RunWith(tx).
|
|
QueryRow().
|
|
Scan(&endTime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = b.saveEvent(tx, event.Status{
|
|
Status: atc.BuildStatus(status),
|
|
Time: endTime.Unix(),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = tx.Exec(fmt.Sprintf(`
|
|
DROP SEQUENCE %s
|
|
`, buildEventSeq(b.id)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if b.jobID != 0 && status == BuildStatusSucceeded {
|
|
_, err = psql.Delete("build_image_resource_caches birc USING builds b").
|
|
Where(sq.Expr("birc.build_id = b.id")).
|
|
Where(sq.Lt{"build_id": b.id}).
|
|
Where(sq.Eq{"b.job_id": b.jobID}).
|
|
RunWith(tx).
|
|
Exec()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if b.jobID != 0 {
|
|
err = bumpCacheIndex(tx, b.pipelineID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = updateTransitionBuildForJob(tx, b.jobID, b.id, status)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = updateLatestCompletedBuildForJob(tx, b.jobID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = updateNextBuildForJob(tx, b.jobID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
err = tx.Commit()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = b.conn.Bus().Notify(buildEventsChannel(b.id))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *build) SetDrained(drained bool) error {
|
|
_, err := psql.Update("builds").
|
|
Set("drained", drained).
|
|
Where(sq.Eq{"id": b.id}).
|
|
RunWith(b.conn).
|
|
Exec()
|
|
|
|
if err == nil {
|
|
b.drained = drained
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (b *build) Delete() (bool, error) {
|
|
rows, err := psql.Delete("builds").
|
|
Where(sq.Eq{
|
|
"id": b.id,
|
|
}).
|
|
RunWith(b.conn).
|
|
Exec()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
affected, err := rows.RowsAffected()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if affected == 0 {
|
|
return false, ErrBuildDisappeared
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// MarkAsAborted will send the abort notification to all build abort
|
|
// channel listeners. It will set the status to aborted that will make
|
|
// AbortNotifier send notification in case if tracking ATC misses the first
|
|
// notification on abort channel.
|
|
// Setting status as aborted will also make Start() return false in case where
|
|
// build was aborted before it was started.
|
|
func (b *build) MarkAsAborted() error {
|
|
_, err := psql.Update("builds").
|
|
Set("aborted", true).
|
|
Where(sq.Eq{"id": b.id}).
|
|
RunWith(b.conn).
|
|
Exec()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return b.conn.Bus().Notify(buildAbortChannel(b.id))
|
|
}
|
|
|
|
// AbortNotifier returns a Notifier that can be watched for when the build
|
|
// is marked as aborted. Once the build is marked as aborted it will send a
|
|
// notification to finish the build to ATC that is tracking this build.
|
|
func (b *build) AbortNotifier() (Notifier, error) {
|
|
return newConditionNotifier(b.conn.Bus(), buildAbortChannel(b.id), func() (bool, error) {
|
|
var aborted bool
|
|
err := psql.Select("aborted = true").
|
|
From("builds").
|
|
Where(sq.Eq{"id": b.id}).
|
|
RunWith(b.conn).
|
|
QueryRow().
|
|
Scan(&aborted)
|
|
|
|
return aborted, err
|
|
})
|
|
}
|
|
|
|
func (b *build) Schedule() (bool, error) {
|
|
result, err := psql.Update("builds").
|
|
Set("scheduled", true).
|
|
Where(sq.Eq{"id": b.id}).
|
|
RunWith(b.conn).
|
|
Exec()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
rows, err := result.RowsAffected()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return rows == 1, nil
|
|
}
|
|
|
|
func (b *build) Pipeline() (Pipeline, bool, error) {
|
|
if b.pipelineID == 0 {
|
|
return nil, false, nil
|
|
}
|
|
|
|
row := pipelinesQuery.
|
|
Where(sq.Eq{"p.id": b.pipelineID}).
|
|
RunWith(b.conn).
|
|
QueryRow()
|
|
|
|
pipeline := newPipeline(b.conn, b.lockFactory)
|
|
err := scanPipeline(pipeline, row)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, false, nil
|
|
}
|
|
return nil, false, err
|
|
}
|
|
|
|
return pipeline, true, nil
|
|
}
|
|
|
|
func (b *build) SaveImageResourceVersion(rc UsedResourceCache) error {
|
|
_, err := psql.Insert("build_image_resource_caches").
|
|
Columns("resource_cache_id", "build_id").
|
|
Values(rc.ID(), b.id).
|
|
RunWith(b.conn).
|
|
Exec()
|
|
if err != nil {
|
|
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code.Name() == pqUniqueViolationErrCode {
|
|
return nil
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *build) AcquireTrackingLock(logger lager.Logger, interval time.Duration) (lock.Lock, bool, error) {
|
|
lock, acquired, err := b.lockFactory.Acquire(
|
|
logger.Session("lock", lager.Data{
|
|
"build_id": b.id,
|
|
}),
|
|
lock.NewBuildTrackingLockID(b.id),
|
|
)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
if !acquired {
|
|
return nil, false, nil
|
|
}
|
|
|
|
return lock, true, nil
|
|
}
|
|
|
|
func (b *build) Preparation() (BuildPreparation, bool, error) {
|
|
if b.jobID == 0 || b.status != BuildStatusPending {
|
|
return BuildPreparation{
|
|
BuildID: b.id,
|
|
PausedPipeline: BuildPreparationStatusNotBlocking,
|
|
PausedJob: BuildPreparationStatusNotBlocking,
|
|
MaxRunningBuilds: BuildPreparationStatusNotBlocking,
|
|
Inputs: map[string]BuildPreparationStatus{},
|
|
InputsSatisfied: BuildPreparationStatusNotBlocking,
|
|
MissingInputReasons: MissingInputReasons{},
|
|
}, true, nil
|
|
}
|
|
|
|
var (
|
|
pausedPipeline bool
|
|
pausedJob bool
|
|
maxInFlightReached bool
|
|
pipelineID int
|
|
jobName string
|
|
)
|
|
err := psql.Select("p.paused, j.paused, j.max_in_flight_reached, j.pipeline_id, j.name").
|
|
From("builds b").
|
|
Join("jobs j ON b.job_id = j.id").
|
|
Join("pipelines p ON j.pipeline_id = p.id").
|
|
Where(sq.Eq{"b.id": b.id}).
|
|
RunWith(b.conn).
|
|
QueryRow().
|
|
Scan(&pausedPipeline, &pausedJob, &maxInFlightReached, &pipelineID, &jobName)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return BuildPreparation{}, false, nil
|
|
}
|
|
return BuildPreparation{}, false, err
|
|
}
|
|
|
|
pausedPipelineStatus := BuildPreparationStatusNotBlocking
|
|
if pausedPipeline {
|
|
pausedPipelineStatus = BuildPreparationStatusBlocking
|
|
}
|
|
|
|
pausedJobStatus := BuildPreparationStatusNotBlocking
|
|
if pausedJob {
|
|
pausedJobStatus = BuildPreparationStatusBlocking
|
|
}
|
|
|
|
maxInFlightReachedStatus := BuildPreparationStatusNotBlocking
|
|
if maxInFlightReached {
|
|
maxInFlightReachedStatus = BuildPreparationStatusBlocking
|
|
}
|
|
|
|
tf := NewTeamFactory(b.conn, b.lockFactory)
|
|
t, found, err := tf.FindTeam(b.teamName)
|
|
if err != nil {
|
|
return BuildPreparation{}, false, err
|
|
}
|
|
|
|
if !found {
|
|
return BuildPreparation{}, false, nil
|
|
}
|
|
|
|
pipeline, found, err := t.Pipeline(b.pipelineName)
|
|
if err != nil {
|
|
return BuildPreparation{}, false, err
|
|
}
|
|
|
|
if !found {
|
|
return BuildPreparation{}, false, nil
|
|
}
|
|
|
|
job, found, err := pipeline.Job(jobName)
|
|
if err != nil {
|
|
return BuildPreparation{}, false, err
|
|
}
|
|
|
|
if !found {
|
|
return BuildPreparation{}, false, nil
|
|
}
|
|
|
|
configInputs := job.Config().Inputs()
|
|
|
|
nextBuildInputs, found, err := job.GetNextBuildInputs()
|
|
if err != nil {
|
|
return BuildPreparation{}, false, err
|
|
}
|
|
|
|
inputsSatisfiedStatus := BuildPreparationStatusBlocking
|
|
inputs := map[string]BuildPreparationStatus{}
|
|
missingInputReasons := MissingInputReasons{}
|
|
|
|
if found {
|
|
|
|
inputsSatisfiedStatus = BuildPreparationStatusNotBlocking
|
|
|
|
if b.IsManuallyTriggered() {
|
|
for _, buildInput := range nextBuildInputs {
|
|
resource, _, err := pipeline.ResourceByID(buildInput.ResourceID)
|
|
if err != nil {
|
|
return BuildPreparation{}, false, err
|
|
}
|
|
|
|
// input is blocking if its last check time is before build create time
|
|
if resource.LastCheckEndTime().Before(b.CreateTime()) {
|
|
inputs[buildInput.Name] = BuildPreparationStatusBlocking
|
|
missingInputReasons.RegisterNoResourceCheckFinished(buildInput.Name)
|
|
inputsSatisfiedStatus = BuildPreparationStatusBlocking
|
|
} else {
|
|
inputs[buildInput.Name] = BuildPreparationStatusNotBlocking
|
|
}
|
|
}
|
|
} else {
|
|
for _, buildInput := range nextBuildInputs {
|
|
inputs[buildInput.Name] = BuildPreparationStatusNotBlocking
|
|
}
|
|
}
|
|
} else {
|
|
buildInputs, err := job.GetIndependentBuildInputs()
|
|
if err != nil {
|
|
return BuildPreparation{}, false, err
|
|
}
|
|
|
|
for _, configInput := range configInputs {
|
|
found := false
|
|
for _, buildInput := range buildInputs {
|
|
if buildInput.Name == configInput.Name {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if found {
|
|
inputs[configInput.Name] = BuildPreparationStatusNotBlocking
|
|
} else {
|
|
inputs[configInput.Name] = BuildPreparationStatusBlocking
|
|
if len(configInput.Passed) > 0 {
|
|
if configInput.Version != nil && configInput.Version.Pinned != nil {
|
|
versionJSON, err := json.Marshal(configInput.Version.Pinned)
|
|
if err != nil {
|
|
return BuildPreparation{}, false, err
|
|
}
|
|
|
|
resource, found, err := pipeline.Resource(configInput.Resource)
|
|
if err != nil {
|
|
return BuildPreparation{}, false, err
|
|
}
|
|
|
|
if found {
|
|
_, found, err = resource.ResourceConfigVersionID(configInput.Version.Pinned)
|
|
if err != nil {
|
|
return BuildPreparation{}, false, err
|
|
}
|
|
|
|
if found {
|
|
missingInputReasons.RegisterPassedConstraint(configInput.Name)
|
|
} else {
|
|
missingInputReasons.RegisterPinnedVersionUnavailable(configInput.Name, string(versionJSON))
|
|
}
|
|
} else {
|
|
missingInputReasons.RegisterPinnedVersionUnavailable(configInput.Name, string(versionJSON))
|
|
}
|
|
} else {
|
|
missingInputReasons.RegisterPassedConstraint(configInput.Name)
|
|
}
|
|
} else {
|
|
if configInput.Version != nil && configInput.Version.Pinned != nil {
|
|
versionJSON, err := json.Marshal(configInput.Version.Pinned)
|
|
if err != nil {
|
|
return BuildPreparation{}, false, err
|
|
}
|
|
|
|
missingInputReasons.RegisterPinnedVersionUnavailable(configInput.Name, string(versionJSON))
|
|
} else {
|
|
missingInputReasons.RegisterNoVersions(configInput.Name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
buildPreparation := BuildPreparation{
|
|
BuildID: b.id,
|
|
PausedPipeline: pausedPipelineStatus,
|
|
PausedJob: pausedJobStatus,
|
|
MaxRunningBuilds: maxInFlightReachedStatus,
|
|
Inputs: inputs,
|
|
InputsSatisfied: inputsSatisfiedStatus,
|
|
MissingInputReasons: missingInputReasons,
|
|
}
|
|
|
|
return buildPreparation, true, nil
|
|
}
|
|
|
|
func (b *build) Events(from uint) (EventSource, error) {
|
|
notifier, err := newConditionNotifier(b.conn.Bus(), buildEventsChannel(b.id), func() (bool, error) {
|
|
return true, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
table := fmt.Sprintf("team_build_events_%d", b.teamID)
|
|
if b.pipelineID != 0 {
|
|
table = fmt.Sprintf("pipeline_build_events_%d", b.pipelineID)
|
|
}
|
|
|
|
return newBuildEventSource(
|
|
b.id,
|
|
table,
|
|
b.conn,
|
|
notifier,
|
|
from,
|
|
), nil
|
|
}
|
|
|
|
func (b *build) SaveEvent(event atc.Event) error {
|
|
tx, err := b.conn.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer Rollback(tx)
|
|
|
|
err = b.saveEvent(tx, event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = tx.Commit()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return b.conn.Bus().Notify(buildEventsChannel(b.id))
|
|
}
|
|
|
|
func (b *build) Artifact(artifactID int) (WorkerArtifact, error) {
|
|
|
|
artifact := artifact{
|
|
conn: b.conn,
|
|
}
|
|
|
|
err := psql.Select("id", "name", "created_at").
|
|
From("worker_artifacts").
|
|
Where(sq.Eq{
|
|
"id": artifactID,
|
|
}).
|
|
RunWith(b.conn).
|
|
Scan(&artifact.id, &artifact.name, &artifact.createdAt)
|
|
|
|
return &artifact, err
|
|
}
|
|
|
|
func (b *build) Artifacts() ([]WorkerArtifact, error) {
|
|
artifacts := []WorkerArtifact{}
|
|
|
|
rows, err := psql.Select("id", "name", "created_at").
|
|
From("worker_artifacts").
|
|
Where(sq.Eq{
|
|
"build_id": b.id,
|
|
}).
|
|
RunWith(b.conn).
|
|
Query()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer Close(rows)
|
|
|
|
for rows.Next() {
|
|
wa := artifact{
|
|
conn: b.conn,
|
|
buildID: b.id,
|
|
}
|
|
|
|
err = rows.Scan(&wa.id, &wa.name, &wa.createdAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
artifacts = append(artifacts, &wa)
|
|
}
|
|
|
|
return artifacts, nil
|
|
}
|
|
|
|
func (b *build) SaveOutput(
|
|
resourceType string,
|
|
source atc.Source,
|
|
resourceTypes atc.VersionedResourceTypes,
|
|
version atc.Version,
|
|
metadata ResourceConfigMetadataFields,
|
|
outputName string,
|
|
resourceName string,
|
|
) error {
|
|
// We should never save outputs for builds without a Pipeline ID because
|
|
// One-off Builds will never have Put steps. This shouldn't happen, but
|
|
// its best to return an error just in case
|
|
if b.pipelineID == 0 {
|
|
return ErrBuildHasNoPipeline
|
|
}
|
|
|
|
pipeline, found, err := b.Pipeline()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !found {
|
|
return ErrBuildHasNoPipeline
|
|
}
|
|
|
|
resource, found, err := pipeline.Resource(resourceName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !found {
|
|
return ResourceNotFoundInPipeline{resourceName, b.pipelineName}
|
|
}
|
|
|
|
tx, err := b.conn.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer Rollback(tx)
|
|
|
|
resourceConfigDescriptor, err := constructResourceConfigDescriptor(resourceType, source, resourceTypes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resourceConfig, err := resourceConfigDescriptor.findOrCreate(tx, b.lockFactory, b.conn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resourceConfigScope, err := findOrCreateResourceConfigScope(tx, b.conn, b.lockFactory, resourceConfig, resource, resourceTypes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newVersion, err := saveResourceVersion(tx, resourceConfigScope, version, metadata)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
versionBytes, err := json.Marshal(version)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
versionJSON := string(versionBytes)
|
|
|
|
if newVersion {
|
|
err = incrementCheckOrder(tx, resourceConfigScope, versionJSON)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
_, err = psql.Insert("build_resource_config_version_outputs").
|
|
Columns("resource_id", "build_id", "version_md5", "name").
|
|
Values(resource.ID(), strconv.Itoa(b.id), sq.Expr("md5(?)", versionJSON), outputName).
|
|
Suffix("ON CONFLICT DO NOTHING").
|
|
RunWith(tx).
|
|
Exec()
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = bumpCacheIndex(tx, b.pipelineID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = tx.Commit()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = bumpCacheIndexForPipelinesUsingResourceConfigScope(b.conn, resourceConfigScope.ID())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *build) UseInputs(inputs []BuildInput) error {
|
|
tx, err := b.conn.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer Rollback(tx)
|
|
|
|
_, err = psql.Delete("build_resource_config_version_inputs").
|
|
Where(sq.Eq{"build_id": b.id}).
|
|
RunWith(tx).
|
|
Exec()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, input := range inputs {
|
|
err = b.saveInputTx(tx, b.id, input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if b.pipelineID != 0 {
|
|
err = bumpCacheIndex(tx, b.pipelineID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (b *build) Resources() ([]BuildInput, []BuildOutput, error) {
|
|
inputs := []BuildInput{}
|
|
outputs := []BuildOutput{}
|
|
|
|
firstOccurrence := `
|
|
NOT EXISTS (
|
|
SELECT 1
|
|
FROM build_resource_config_version_inputs i, builds b
|
|
WHERE versions.version_md5 = i.version_md5
|
|
AND resources.resource_config_scope_id = versions.resource_config_scope_id
|
|
AND resources.id = i.resource_id
|
|
AND b.job_id = builds.job_id
|
|
AND i.build_id = b.id
|
|
AND i.build_id < builds.id
|
|
)`
|
|
|
|
rows, err := psql.Select("inputs.name", "resources.id", "versions.version", firstOccurrence).
|
|
From("resource_config_versions versions, build_resource_config_version_inputs inputs, builds, resources").
|
|
Where(sq.Eq{"builds.id": b.id}).
|
|
Where(sq.NotEq{"versions.check_order": 0}).
|
|
Where(sq.Expr("inputs.build_id = builds.id")).
|
|
Where(sq.Expr("inputs.version_md5 = versions.version_md5")).
|
|
Where(sq.Expr("resources.resource_config_scope_id = versions.resource_config_scope_id")).
|
|
Where(sq.Expr("resources.id = inputs.resource_id")).
|
|
Where(sq.Expr(`NOT EXISTS (
|
|
SELECT 1
|
|
FROM build_resource_config_version_outputs outputs
|
|
WHERE outputs.version_md5 = versions.version_md5
|
|
AND versions.resource_config_scope_id = resources.resource_config_scope_id
|
|
AND outputs.resource_id = resources.id
|
|
AND outputs.build_id = inputs.build_id
|
|
)`)).
|
|
RunWith(b.conn).
|
|
Query()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
defer Close(rows)
|
|
|
|
for rows.Next() {
|
|
var (
|
|
inputName string
|
|
firstOccurrence bool
|
|
versionBlob string
|
|
version atc.Version
|
|
resourceID int
|
|
)
|
|
|
|
err = rows.Scan(&inputName, &resourceID, &versionBlob, &firstOccurrence)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
err = json.Unmarshal([]byte(versionBlob), &version)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
inputs = append(inputs, BuildInput{
|
|
Name: inputName,
|
|
Version: version,
|
|
ResourceID: resourceID,
|
|
FirstOccurrence: firstOccurrence,
|
|
})
|
|
}
|
|
|
|
rows, err = psql.Select("outputs.name", "versions.version").
|
|
From("resource_config_versions versions, build_resource_config_version_outputs outputs, builds, resources").
|
|
Where(sq.Eq{"builds.id": b.id}).
|
|
Where(sq.NotEq{"versions.check_order": 0}).
|
|
Where(sq.Expr("outputs.build_id = builds.id")).
|
|
Where(sq.Expr("outputs.version_md5 = versions.version_md5")).
|
|
Where(sq.Expr("outputs.resource_id = resources.id")).
|
|
Where(sq.Expr("resources.resource_config_scope_id = versions.resource_config_scope_id")).
|
|
RunWith(b.conn).
|
|
Query()
|
|
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
defer Close(rows)
|
|
|
|
for rows.Next() {
|
|
var (
|
|
outputName string
|
|
versionBlob string
|
|
version atc.Version
|
|
)
|
|
|
|
err := rows.Scan(&outputName, &versionBlob)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
err = json.Unmarshal([]byte(versionBlob), &version)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
outputs = append(outputs, BuildOutput{
|
|
Name: outputName,
|
|
Version: version,
|
|
})
|
|
}
|
|
|
|
return inputs, outputs, nil
|
|
}
|
|
|
|
func (p *build) saveInputTx(tx Tx, buildID int, input BuildInput) error {
|
|
versionJSON, err := json.Marshal(input.Version)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = psql.Insert("build_resource_config_version_inputs").
|
|
Columns("build_id", "resource_id", "version_md5", "name").
|
|
Values(buildID, input.ResourceID, sq.Expr("md5(?)", versionJSON), input.Name).
|
|
Suffix("ON CONFLICT DO NOTHING").
|
|
RunWith(tx).
|
|
Exec()
|
|
|
|
return err
|
|
}
|
|
|
|
func createBuildEventSeq(tx Tx, buildid int) error {
|
|
_, err := tx.Exec(fmt.Sprintf(`
|
|
CREATE SEQUENCE %s MINVALUE 0
|
|
`, buildEventSeq(buildid)))
|
|
return err
|
|
}
|
|
|
|
func buildEventSeq(buildid int) string {
|
|
return fmt.Sprintf("build_event_id_seq_%d", buildid)
|
|
}
|
|
|
|
func scanBuild(b *build, row scannable, encryptionStrategy encryption.Strategy) error {
|
|
var (
|
|
jobID, pipelineID sql.NullInt64
|
|
schema, privatePlan, jobName, pipelineName, publicPlan sql.NullString
|
|
createTime, startTime, endTime, reapTime pq.NullTime
|
|
nonce sql.NullString
|
|
drained, aborted, completed bool
|
|
status string
|
|
)
|
|
|
|
err := row.Scan(&b.id, &b.name, &jobID, &b.teamID, &status, &b.isManuallyTriggered, &b.scheduled, &schema, &privatePlan, &publicPlan, &createTime, &startTime, &endTime, &reapTime, &jobName, &pipelineID, &pipelineName, &b.teamName, &nonce, &drained, &aborted, &completed)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
b.status = BuildStatus(status)
|
|
b.jobName = jobName.String
|
|
b.jobID = int(jobID.Int64)
|
|
b.pipelineName = pipelineName.String
|
|
b.pipelineID = int(pipelineID.Int64)
|
|
b.schema = schema.String
|
|
b.createTime = createTime.Time
|
|
b.startTime = startTime.Time
|
|
b.endTime = endTime.Time
|
|
b.reapTime = reapTime.Time
|
|
b.drained = drained
|
|
b.aborted = aborted
|
|
b.completed = completed
|
|
|
|
var (
|
|
noncense *string
|
|
decryptedPlan []byte
|
|
)
|
|
|
|
if nonce.Valid {
|
|
noncense = &nonce.String
|
|
decryptedPlan, err = encryptionStrategy.Decrypt(string(privatePlan.String), noncense)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
decryptedPlan = []byte(privatePlan.String)
|
|
}
|
|
|
|
if len(decryptedPlan) > 0 {
|
|
err = json.Unmarshal(decryptedPlan, &b.privatePlan)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if publicPlan.Valid {
|
|
err = json.Unmarshal([]byte(publicPlan.String), &b.publicPlan)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *build) saveEvent(tx Tx, event atc.Event) error {
|
|
payload, err := json.Marshal(event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
table := fmt.Sprintf("team_build_events_%d", b.teamID)
|
|
if b.pipelineID != 0 {
|
|
table = fmt.Sprintf("pipeline_build_events_%d", b.pipelineID)
|
|
}
|
|
_, err = psql.Insert(table).
|
|
Columns("event_id", "build_id", "type", "version", "payload").
|
|
Values(sq.Expr("nextval('"+buildEventSeq(b.id)+"')"), b.id, string(event.EventType()), string(event.Version()), payload).
|
|
RunWith(tx).
|
|
Exec()
|
|
return err
|
|
}
|
|
|
|
func createBuild(tx Tx, build *build, vals map[string]interface{}) error {
|
|
var buildID int
|
|
err := psql.Insert("builds").
|
|
SetMap(vals).
|
|
Suffix("RETURNING id").
|
|
RunWith(tx).
|
|
QueryRow().
|
|
Scan(&buildID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = scanBuild(build, buildsQuery.
|
|
Where(sq.Eq{"b.id": buildID}).
|
|
RunWith(tx).
|
|
QueryRow(),
|
|
build.conn.EncryptionStrategy(),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return createBuildEventSeq(tx, buildID)
|
|
}
|
|
|
|
func buildStartedChannel() string {
|
|
return fmt.Sprintf("build_started")
|
|
}
|
|
|
|
func buildEventsChannel(buildID int) string {
|
|
return fmt.Sprintf("build_events_%d", buildID)
|
|
}
|
|
|
|
func buildAbortChannel(buildID int) string {
|
|
return fmt.Sprintf("build_abort_%d", buildID)
|
|
}
|
|
|
|
func updateNextBuildForJob(tx Tx, jobID int) error {
|
|
_, err := tx.Exec(`
|
|
UPDATE jobs AS j
|
|
SET next_build_id = (
|
|
SELECT min(b.id)
|
|
FROM builds b
|
|
WHERE b.job_id = $1
|
|
AND b.status IN ('pending', 'started')
|
|
)
|
|
WHERE j.id = $1
|
|
`, jobID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func updateLatestCompletedBuildForJob(tx Tx, jobID int) error {
|
|
_, err := tx.Exec(`
|
|
UPDATE jobs AS j
|
|
SET latest_completed_build_id = (
|
|
SELECT max(b.id)
|
|
FROM builds b
|
|
WHERE b.job_id = $1
|
|
AND b.status NOT IN ('pending', 'started')
|
|
)
|
|
WHERE j.id = $1
|
|
`, jobID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func updateTransitionBuildForJob(tx Tx, jobID int, buildID int, buildStatus BuildStatus) error {
|
|
var shouldUpdateTransition bool
|
|
|
|
var latestID int
|
|
var latestStatus BuildStatus
|
|
err := psql.Select("b.id", "b.status").
|
|
From("builds b").
|
|
JoinClause("INNER JOIN jobs j ON j.latest_completed_build_id = b.id").
|
|
Where(sq.Eq{"j.id": jobID}).
|
|
RunWith(tx).
|
|
QueryRow().
|
|
Scan(&latestID, &latestStatus)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
// this is the first completed build; initiate transition
|
|
shouldUpdateTransition = true
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if buildID < latestID {
|
|
// latest completed build is actually after this one, so this build
|
|
// has no influence on the job's overall state
|
|
//
|
|
// this can happen when multiple builds are running at a time and the
|
|
// later-queued ones finish earlier
|
|
return nil
|
|
}
|
|
|
|
if latestStatus != buildStatus {
|
|
// status has changed; transitioned!
|
|
shouldUpdateTransition = true
|
|
}
|
|
|
|
if shouldUpdateTransition {
|
|
_, err := psql.Update("jobs").
|
|
Set("transition_build_id", buildID).
|
|
Where(sq.Eq{"id": jobID}).
|
|
RunWith(tx).
|
|
Exec()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|