471 lines
12 KiB
Go
471 lines
12 KiB
Go
package radar
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"time"
|
|
|
|
"code.cloudfoundry.org/clock"
|
|
"code.cloudfoundry.org/lager"
|
|
"github.com/concourse/concourse/atc"
|
|
"github.com/concourse/concourse/atc/creds"
|
|
"github.com/concourse/concourse/atc/db"
|
|
"github.com/concourse/concourse/atc/metric"
|
|
"github.com/concourse/concourse/atc/resource"
|
|
"github.com/concourse/concourse/atc/worker"
|
|
)
|
|
|
|
var GlobalResourceCheckTimeout time.Duration
|
|
|
|
type resourceScanner struct {
|
|
clock clock.Clock
|
|
pool worker.Pool
|
|
resourceFactory resource.ResourceFactory
|
|
resourceConfigFactory db.ResourceConfigFactory
|
|
defaultInterval time.Duration
|
|
dbPipeline db.Pipeline
|
|
externalURL string
|
|
variables creds.Variables
|
|
strategy worker.ContainerPlacementStrategy
|
|
}
|
|
|
|
func NewResourceScanner(
|
|
clock clock.Clock,
|
|
pool worker.Pool,
|
|
resourceFactory resource.ResourceFactory,
|
|
resourceConfigFactory db.ResourceConfigFactory,
|
|
defaultInterval time.Duration,
|
|
dbPipeline db.Pipeline,
|
|
externalURL string,
|
|
variables creds.Variables,
|
|
strategy worker.ContainerPlacementStrategy,
|
|
) Scanner {
|
|
return &resourceScanner{
|
|
clock: clock,
|
|
pool: pool,
|
|
resourceFactory: resourceFactory,
|
|
resourceConfigFactory: resourceConfigFactory,
|
|
defaultInterval: defaultInterval,
|
|
dbPipeline: dbPipeline,
|
|
externalURL: externalURL,
|
|
variables: variables,
|
|
strategy: strategy,
|
|
}
|
|
}
|
|
|
|
var ErrFailedToAcquireLock = errors.New("failed to acquire lock")
|
|
var ErrResourceTypeNotFound = errors.New("resource type not found")
|
|
var ErrResourceTypeCheckError = errors.New("resource type failed to check")
|
|
|
|
func (scanner *resourceScanner) Run(logger lager.Logger, resourceID int) (time.Duration, error) {
|
|
interval, err := scanner.scan(logger.Session("tick"), resourceID, nil, false, false)
|
|
|
|
err = swallowErrResourceScriptFailed(err)
|
|
|
|
return interval, err
|
|
}
|
|
|
|
func (scanner *resourceScanner) ScanFromVersion(logger lager.Logger, resourceID int, fromVersion atc.Version) error {
|
|
_, err := scanner.scan(logger, resourceID, fromVersion, true, true)
|
|
|
|
return err
|
|
}
|
|
|
|
func (scanner *resourceScanner) Scan(logger lager.Logger, resourceID int) error {
|
|
_, err := scanner.scan(logger, resourceID, nil, true, false)
|
|
|
|
err = swallowErrResourceScriptFailed(err)
|
|
|
|
return err
|
|
}
|
|
|
|
func (scanner *resourceScanner) scan(logger lager.Logger, resourceID int, fromVersion atc.Version, mustComplete bool, saveGiven bool) (time.Duration, error) {
|
|
savedResource, found, err := scanner.dbPipeline.ResourceByID(resourceID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if !found {
|
|
logger.Debug("resource-not-found")
|
|
return 0, db.ResourceNotFoundError{ID: resourceID}
|
|
}
|
|
|
|
lockLogger := logger.Session("lock", lager.Data{
|
|
"resource": savedResource.Name(),
|
|
})
|
|
|
|
timeout, err := scanner.parseResourceCheckTimeoutOrDefault(savedResource.CheckTimeout())
|
|
if err != nil {
|
|
scanner.setResourceCheckError(logger, savedResource, err)
|
|
logger.Error("failed-to-read-check-timeout", err)
|
|
return 0, err
|
|
}
|
|
|
|
interval, err := scanner.checkInterval(savedResource.CheckEvery())
|
|
if err != nil {
|
|
scanner.setResourceCheckError(logger, savedResource, err)
|
|
logger.Error("failed-to-read-check-interval", err)
|
|
return 0, err
|
|
}
|
|
|
|
resourceTypes, err := scanner.dbPipeline.ResourceTypes()
|
|
if err != nil {
|
|
logger.Error("failed-to-get-resource-types", err)
|
|
return 0, err
|
|
}
|
|
|
|
for _, parentType := range resourceTypes {
|
|
if parentType.Name() != savedResource.Type() {
|
|
continue
|
|
}
|
|
|
|
for {
|
|
if parentType.Version() != nil {
|
|
break
|
|
}
|
|
|
|
if parentType.CheckError() != nil {
|
|
scanner.setResourceCheckError(logger, savedResource, parentType.CheckError())
|
|
logger.Error("resource-type-failed-to-check", err, lager.Data{"resource-type": parentType.Name()})
|
|
return 0, ErrResourceTypeCheckError
|
|
} else {
|
|
logger.Debug("waiting-on-resource-type-version", lager.Data{"resource-type": parentType.Name()})
|
|
scanner.clock.Sleep(10 * time.Second)
|
|
|
|
found, err := parentType.Reload()
|
|
if err != nil {
|
|
logger.Error("failed-to-reload-resource-type", err, lager.Data{"resource-type": parentType.Name()})
|
|
return 0, err
|
|
}
|
|
|
|
if !found {
|
|
logger.Error("resource-type-not-found", err, lager.Data{"resource-type": parentType.Name()})
|
|
return 0, ErrResourceTypeNotFound
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
resourceTypes, err = scanner.dbPipeline.ResourceTypes()
|
|
if err != nil {
|
|
logger.Error("failed-to-get-resource-types", err)
|
|
return 0, err
|
|
}
|
|
|
|
versionedResourceTypes, err := creds.NewVersionedResourceTypes(
|
|
scanner.variables,
|
|
resourceTypes.Deserialize(),
|
|
).Evaluate()
|
|
if err != nil {
|
|
logger.Error("failed-to-evaluate-resource-types", err)
|
|
scanner.setResourceCheckError(logger, savedResource, err)
|
|
return 0, err
|
|
}
|
|
|
|
source, err := creds.NewSource(scanner.variables, savedResource.Source()).Evaluate()
|
|
if err != nil {
|
|
logger.Error("failed-to-evaluate-resource-source", err)
|
|
scanner.setResourceCheckError(logger, savedResource, err)
|
|
return 0, err
|
|
}
|
|
|
|
resourceConfigScope, err := savedResource.SetResourceConfig(
|
|
source,
|
|
versionedResourceTypes,
|
|
)
|
|
if err != nil {
|
|
logger.Error("failed-to-set-resource-config-id-on-resource", err)
|
|
scanner.setResourceCheckError(logger, savedResource, err)
|
|
return 0, err
|
|
}
|
|
|
|
// Clear out check error on the resource
|
|
scanner.setResourceCheckError(logger, savedResource, nil)
|
|
|
|
currentVersion := savedResource.CurrentPinnedVersion()
|
|
if currentVersion != nil {
|
|
_, found, err := resourceConfigScope.FindVersion(currentVersion)
|
|
|
|
if err != nil {
|
|
logger.Error("failed-to-find-pinned-version-on-resource", err, lager.Data{"pinned-version": currentVersion})
|
|
chkErr := resourceConfigScope.SetCheckError(err)
|
|
if chkErr != nil {
|
|
logger.Error("failed-to-set-check-error-on-resource-config", chkErr)
|
|
}
|
|
return 0, err
|
|
}
|
|
if found {
|
|
logger.Info("skipping-check-because-pinned-version-found", lager.Data{"pinned-version": currentVersion})
|
|
return interval, nil
|
|
}
|
|
|
|
fromVersion = currentVersion
|
|
}
|
|
|
|
for {
|
|
lock, acquired, err := resourceConfigScope.AcquireResourceCheckingLock(
|
|
logger,
|
|
)
|
|
if err != nil {
|
|
lockLogger.Error("failed-to-get-lock", err, lager.Data{
|
|
"resource_name": savedResource.Name(),
|
|
"resource_config": resourceConfigScope.ResourceConfig().ID(),
|
|
})
|
|
return interval, ErrFailedToAcquireLock
|
|
}
|
|
|
|
if !acquired {
|
|
lockLogger.Debug("did-not-get-lock")
|
|
scanner.clock.Sleep(time.Second)
|
|
continue
|
|
}
|
|
|
|
defer lock.Release()
|
|
|
|
updated, err := resourceConfigScope.UpdateLastCheckStartTime(interval, mustComplete)
|
|
if err != nil {
|
|
return interval, err
|
|
}
|
|
|
|
if !updated {
|
|
logger.Debug("interval-not-reached", lager.Data{
|
|
"interval": interval,
|
|
})
|
|
return interval, ErrFailedToAcquireLock
|
|
}
|
|
|
|
break
|
|
}
|
|
|
|
if fromVersion == nil {
|
|
rcv, found, err := resourceConfigScope.LatestVersion()
|
|
if err != nil {
|
|
logger.Error("failed-to-get-current-version", err)
|
|
return interval, err
|
|
}
|
|
|
|
if found {
|
|
fromVersion = atc.Version(rcv.Version())
|
|
}
|
|
}
|
|
|
|
return interval, scanner.check(
|
|
logger,
|
|
savedResource,
|
|
resourceConfigScope,
|
|
fromVersion,
|
|
versionedResourceTypes,
|
|
source,
|
|
saveGiven,
|
|
timeout,
|
|
)
|
|
}
|
|
|
|
func (scanner *resourceScanner) check(
|
|
logger lager.Logger,
|
|
savedResource db.Resource,
|
|
resourceConfigScope db.ResourceConfigScope,
|
|
fromVersion atc.Version,
|
|
resourceTypes atc.VersionedResourceTypes,
|
|
source atc.Source,
|
|
saveGiven bool,
|
|
timeout time.Duration,
|
|
) error {
|
|
pipelinePaused, err := scanner.dbPipeline.CheckPaused()
|
|
if err != nil {
|
|
logger.Error("failed-to-check-if-pipeline-paused", err)
|
|
return err
|
|
}
|
|
|
|
if pipelinePaused {
|
|
logger.Debug("pipeline-paused")
|
|
return nil
|
|
}
|
|
|
|
found, err := scanner.dbPipeline.Reload()
|
|
if err != nil {
|
|
logger.Error("failed-to-reload-scannerdb", err)
|
|
return err
|
|
}
|
|
if !found {
|
|
logger.Info("pipeline-removed")
|
|
return errPipelineRemoved
|
|
}
|
|
|
|
metadata := resource.TrackerMetadata{
|
|
ResourceName: savedResource.Name(),
|
|
PipelineName: savedResource.PipelineName(),
|
|
ExternalURL: scanner.externalURL,
|
|
}
|
|
|
|
containerSpec := worker.ContainerSpec{
|
|
ImageSpec: worker.ImageSpec{
|
|
ResourceType: savedResource.Type(),
|
|
},
|
|
BindMounts: []worker.BindMountSource{
|
|
&worker.CertsVolumeMount{Logger: logger},
|
|
},
|
|
Tags: savedResource.Tags(),
|
|
TeamID: scanner.dbPipeline.TeamID(),
|
|
Env: metadata.Env(),
|
|
}
|
|
|
|
workerSpec := worker.WorkerSpec{
|
|
ResourceType: savedResource.Type(),
|
|
Tags: savedResource.Tags(),
|
|
ResourceTypes: resourceTypes,
|
|
TeamID: scanner.dbPipeline.TeamID(),
|
|
}
|
|
|
|
owner := db.NewResourceConfigCheckSessionContainerOwner(resourceConfigScope.ResourceConfig(), ContainerExpiries)
|
|
|
|
chosenWorker, err := scanner.pool.FindOrChooseWorkerForContainer(
|
|
context.Background(),
|
|
logger,
|
|
owner,
|
|
containerSpec,
|
|
db.ContainerMetadata{
|
|
Type: db.ContainerTypeCheck,
|
|
},
|
|
workerSpec,
|
|
scanner.strategy,
|
|
)
|
|
if err != nil {
|
|
logger.Error("failed-to-choose-a-worker", err)
|
|
chkErr := resourceConfigScope.SetCheckError(err)
|
|
if chkErr != nil {
|
|
logger.Error("failed-to-set-check-error-on-resource-config", chkErr)
|
|
}
|
|
return err
|
|
}
|
|
container, err := chosenWorker.FindOrCreateContainer(
|
|
context.Background(),
|
|
logger,
|
|
worker.NoopImageFetchingDelegate{},
|
|
owner,
|
|
containerSpec,
|
|
resourceTypes,
|
|
)
|
|
if err != nil {
|
|
// TODO: remove this after ephemeral check containers.
|
|
// Sometimes we pass in a check session thats too close to
|
|
// expirey into FindOrCreateContainer such that the container
|
|
// gced before the call is completed
|
|
if err == worker.ResourceConfigCheckSessionExpiredError {
|
|
return nil
|
|
}
|
|
logger.Error("failed-to-create-or-find-container", err)
|
|
chkErr := resourceConfigScope.SetCheckError(err)
|
|
if chkErr != nil {
|
|
logger.Error("failed-to-set-check-error-on-resource-config", chkErr)
|
|
}
|
|
return err
|
|
}
|
|
|
|
logger.Debug("checking", lager.Data{
|
|
"from": fromVersion,
|
|
})
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
|
|
res := scanner.resourceFactory.NewResourceForContainer(container)
|
|
newVersions, err := res.Check(ctx, source, fromVersion)
|
|
if err == context.DeadlineExceeded {
|
|
err = fmt.Errorf("Timed out after %v while checking for new versions - perhaps increase your resource check timeout?", timeout)
|
|
}
|
|
|
|
resourceConfigScope.SetCheckError(err)
|
|
metric.ResourceCheck{
|
|
PipelineName: scanner.dbPipeline.Name(),
|
|
ResourceName: savedResource.Name(),
|
|
TeamName: scanner.dbPipeline.TeamName(),
|
|
Success: err == nil,
|
|
}.Emit(logger)
|
|
|
|
if err != nil {
|
|
if rErr, ok := err.(resource.ErrResourceScriptFailed); ok {
|
|
logger.Info("check-failed", lager.Data{"exit-status": rErr.ExitStatus})
|
|
return rErr
|
|
}
|
|
|
|
logger.Error("failed-to-check", err)
|
|
return err
|
|
}
|
|
|
|
if len(newVersions) == 0 || (!saveGiven && reflect.DeepEqual(newVersions, []atc.Version{fromVersion})) {
|
|
logger.Debug("no-new-versions")
|
|
} else {
|
|
logger.Info("versions-found", lager.Data{
|
|
"versions": newVersions,
|
|
"total": len(newVersions),
|
|
})
|
|
|
|
err = resourceConfigScope.SaveVersions(newVersions)
|
|
if err != nil {
|
|
logger.Error("failed-to-save-resource-config-versions", err, lager.Data{
|
|
"versions": newVersions,
|
|
})
|
|
|
|
return err
|
|
}
|
|
}
|
|
|
|
updated, err := resourceConfigScope.UpdateLastCheckEndTime()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !updated {
|
|
logger.Debug("did-not-update-last-check-finished")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func swallowErrResourceScriptFailed(err error) error {
|
|
if _, ok := err.(resource.ErrResourceScriptFailed); ok {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (scanner *resourceScanner) parseResourceCheckTimeoutOrDefault(checkTimeout string) (time.Duration, error) {
|
|
interval := GlobalResourceCheckTimeout
|
|
if checkTimeout != "" {
|
|
configuredInterval, err := time.ParseDuration(checkTimeout)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
interval = configuredInterval
|
|
}
|
|
|
|
return interval, nil
|
|
}
|
|
|
|
func (scanner *resourceScanner) checkInterval(checkEvery string) (time.Duration, error) {
|
|
interval := scanner.defaultInterval
|
|
if checkEvery != "" {
|
|
configuredInterval, err := time.ParseDuration(checkEvery)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
interval = configuredInterval
|
|
}
|
|
|
|
return interval, nil
|
|
}
|
|
|
|
func (scanner *resourceScanner) setResourceCheckError(logger lager.Logger, savedResource db.Resource, err error) {
|
|
setErr := savedResource.SetCheckSetupError(err)
|
|
if setErr != nil {
|
|
logger.Error("failed-to-set-check-error", err)
|
|
}
|
|
}
|
|
|
|
var errPipelineRemoved = errors.New("pipeline removed")
|