concourse/atc/atccmd/command.go

1470 lines
44 KiB
Go

package atccmd
import (
"crypto/tls"
"crypto/x509"
"database/sql"
"errors"
"fmt"
"net"
"net/http"
_ "net/http/pprof"
"net/url"
"os"
"strings"
"time"
"code.cloudfoundry.org/clock"
"code.cloudfoundry.org/lager"
"github.com/concourse/concourse"
"github.com/concourse/concourse/atc"
"github.com/concourse/concourse/atc/api"
"github.com/concourse/concourse/atc/api/accessor"
"github.com/concourse/concourse/atc/api/auth"
"github.com/concourse/concourse/atc/api/buildserver"
"github.com/concourse/concourse/atc/api/containerserver"
"github.com/concourse/concourse/atc/auditor"
"github.com/concourse/concourse/atc/builds"
"github.com/concourse/concourse/atc/creds"
"github.com/concourse/concourse/atc/creds/noop"
"github.com/concourse/concourse/atc/db"
"github.com/concourse/concourse/atc/db/encryption"
"github.com/concourse/concourse/atc/db/lock"
"github.com/concourse/concourse/atc/db/migration"
"github.com/concourse/concourse/atc/engine"
"github.com/concourse/concourse/atc/engine/builder"
"github.com/concourse/concourse/atc/gc"
"github.com/concourse/concourse/atc/lockrunner"
"github.com/concourse/concourse/atc/metric"
"github.com/concourse/concourse/atc/pipelines"
"github.com/concourse/concourse/atc/radar"
"github.com/concourse/concourse/atc/resource"
"github.com/concourse/concourse/atc/scheduler"
"github.com/concourse/concourse/atc/syslog"
"github.com/concourse/concourse/atc/worker"
"github.com/concourse/concourse/atc/worker/image"
"github.com/concourse/concourse/atc/wrappa"
"github.com/concourse/concourse/skymarshal"
"github.com/concourse/concourse/skymarshal/skycmd"
"github.com/concourse/concourse/skymarshal/storage"
"github.com/concourse/concourse/web"
"github.com/concourse/flag"
"github.com/concourse/retryhttp"
"github.com/cppforlife/go-semi-semantic/version"
"github.com/hashicorp/go-multierror"
"github.com/jessevdk/go-flags"
"github.com/tedsuo/ifrit"
"github.com/tedsuo/ifrit/grouper"
"github.com/tedsuo/ifrit/http_server"
"github.com/tedsuo/ifrit/sigmon"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
// dynamically registered metric emitters
_ "github.com/concourse/concourse/atc/metric/emitter"
// dynamically registered credential managers
_ "github.com/concourse/concourse/atc/creds/credhub"
_ "github.com/concourse/concourse/atc/creds/kubernetes"
_ "github.com/concourse/concourse/atc/creds/secretsmanager"
_ "github.com/concourse/concourse/atc/creds/ssm"
_ "github.com/concourse/concourse/atc/creds/vault"
)
var defaultDriverName = "postgres"
var retryingDriverName = "too-many-connections-retrying"
type ATCCommand struct {
RunCommand RunCommand `command:"run"`
Migration Migration `command:"migrate"`
}
type RunCommand struct {
Logger flag.Lager
BindIP flag.IP `long:"bind-ip" default:"0.0.0.0" description:"IP address on which to listen for web traffic."`
BindPort uint16 `long:"bind-port" default:"8080" description:"Port on which to listen for HTTP traffic."`
TLSBindPort uint16 `long:"tls-bind-port" description:"Port on which to listen for HTTPS traffic."`
TLSCert flag.File `long:"tls-cert" description:"File containing an SSL certificate."`
TLSKey flag.File `long:"tls-key" description:"File containing an RSA private key, used to encrypt HTTPS traffic."`
LetsEncrypt struct {
Enable bool `long:"enable-lets-encrypt" description:"Automatically configure TLS certificates via Let's Encrypt/ACME."`
ACMEURL flag.URL `long:"lets-encrypt-acme-url" description:"URL of the ACME CA directory endpoint." default:"https://acme-v01.api.letsencrypt.org/directory"`
} `group:"Let's Encrypt Configuration"`
ExternalURL flag.URL `long:"external-url" description:"URL used to reach any ATC from the outside world."`
Postgres flag.PostgresConfig `group:"PostgreSQL Configuration" namespace:"postgres"`
CredentialManagement creds.CredentialManagementConfig `group:"Credential Management"`
CredentialManagers creds.Managers
EncryptionKey flag.Cipher `long:"encryption-key" description:"A 16 or 32 length key used to encrypt sensitive information before storing it in the database."`
OldEncryptionKey flag.Cipher `long:"old-encryption-key" description:"Encryption key previously used for encrypting sensitive information. If provided without a new key, data is encrypted. If provided with a new key, data is re-encrypted."`
DebugBindIP flag.IP `long:"debug-bind-ip" default:"127.0.0.1" description:"IP address on which to listen for the pprof debugger endpoints."`
DebugBindPort uint16 `long:"debug-bind-port" default:"8079" description:"Port on which to listen for the pprof debugger endpoints."`
InterceptIdleTimeout time.Duration `long:"intercept-idle-timeout" default:"0m" description:"Length of time for a intercepted session to be idle before terminating."`
EnableGlobalResources bool `long:"enable-global-resources" description:"Enable equivalent resources across pipelines and teams to share a single version history."`
GlobalResourceCheckTimeout time.Duration `long:"global-resource-check-timeout" default:"1h" description:"Time limit on checking for new versions of resources."`
ResourceCheckingInterval time.Duration `long:"resource-checking-interval" default:"1m" description:"Interval on which to check for new versions of resources."`
ResourceTypeCheckingInterval time.Duration `long:"resource-type-checking-interval" default:"1m" description:"Interval on which to check for new versions of resource types."`
ContainerPlacementStrategy string `long:"container-placement-strategy" default:"volume-locality" choice:"volume-locality" choice:"random" choice:"fewest-build-containers" description:"Method by which a worker is selected during container placement."`
BaggageclaimResponseHeaderTimeout time.Duration `long:"baggageclaim-response-header-timeout" default:"1m" description:"How long to wait for Baggageclaim to send the response header."`
CLIArtifactsDir flag.Dir `long:"cli-artifacts-dir" description:"Directory containing downloadable CLI binaries."`
Developer struct {
Noop bool `short:"n" long:"noop" description:"Don't actually do any automatic scheduling or checking."`
} `group:"Developer Options"`
Worker struct {
GardenURL flag.URL `long:"garden-url" description:"A Garden API endpoint to register as a worker."`
BaggageclaimURL flag.URL `long:"baggageclaim-url" description:"A Baggageclaim API endpoint to register with the worker."`
ResourceTypes map[string]string `long:"resource" description:"A resource type to advertise for the worker. Can be specified multiple times." value-name:"TYPE:IMAGE"`
} `group:"Static Worker (optional)" namespace:"worker"`
Metrics struct {
HostName string `long:"metrics-host-name" description:"Host string to attach to emitted metrics."`
Attributes map[string]string `long:"metrics-attribute" description:"A key-value attribute to attach to emitted metrics. Can be specified multiple times." value-name:"NAME:VALUE"`
CaptureErrorMetrics bool `long:"capture-error-metrics" description:"Enable capturing of error log metrics"`
} `group:"Metrics & Diagnostics"`
Server struct {
XFrameOptions string `long:"x-frame-options" default:"deny" description:"The value to set for X-Frame-Options."`
ClusterName string `long:"cluster-name" description:"A name for this Concourse cluster, to be displayed on the dashboard page."`
} `group:"Web Server"`
LogDBQueries bool `long:"log-db-queries" description:"Log database queries."`
GC struct {
Interval time.Duration `long:"interval" default:"30s" description:"Interval on which to perform garbage collection."`
OneOffBuildGracePeriod time.Duration `long:"one-off-grace-period" default:"5m" description:"Period after which one-off build containers will be garbage-collected."`
MissingGracePeriod time.Duration `long:"missing-grace-period" default:"5m" description:"Period after which to reap containers and volumes that were created but went missing from the worker."`
} `group:"Garbage Collection" namespace:"gc"`
BuildTrackerInterval time.Duration `long:"build-tracker-interval" default:"10s" description:"Interval on which to run build tracking."`
TelemetryOptIn bool `long:"telemetry-opt-in" hidden:"true" description:"Enable anonymous concourse version reporting."`
DefaultBuildLogsToRetain uint64 `long:"default-build-logs-to-retain" description:"Default build logs to retain, 0 means all"`
MaxBuildLogsToRetain uint64 `long:"max-build-logs-to-retain" description:"Maximum build logs to retain, 0 means not specified. Will override values configured in jobs"`
DefaultDaysToRetainBuildLogs uint64 `long:"default-days-to-retain-build-logs" description:"Default days to retain build logs. 0 means unlimited"`
MaxDaysToRetainBuildLogs uint64 `long:"max-days-to-retain-build-logs" description:"Maximum days to retain build logs, 0 means not specified. Will override values configured in jobs"`
DefaultCpuLimit *int `long:"default-task-cpu-limit" description:"Default max number of cpu shares per task, 0 means unlimited"`
DefaultMemoryLimit *string `long:"default-task-memory-limit" description:"Default maximum memory per task, 0 means unlimited"`
Auditor struct {
EnableBuildAuditLog bool `long:"enable-build-auditing" description:"Enable auditing for all api requests connected to builds."`
EnableContainerAuditLog bool `long:"enable-container-auditing" description:"Enable auditing for all api requests connected to containers."`
EnableJobAuditLog bool `long:"enable-job-auditing" description:"Enable auditing for all api requests connected to jobs."`
EnablePipelineAuditLog bool `long:"enable-pipeline-auditing" description:"Enable auditing for all api requests connected to pipelines."`
EnableResourceAuditLog bool `long:"enable-resource-auditing" description:"Enable auditing for all api requests connected to resources."`
EnableSystemAuditLog bool `long:"enable-system-auditing" description:"Enable auditing for all api requests connected to system transactions."`
EnableTeamAuditLog bool `long:"enable-team-auditing" description:"Enable auditing for all api requests connected to teams."`
EnableWorkerAuditLog bool `long:"enable-worker-auditing" description:"Enable auditing for all api requests connected to workers."`
EnableVolumeAuditLog bool `long:"enable-volume-auditing" description:"Enable auditing for all api requests connected to volumes."`
}
Syslog struct {
Hostname string `long:"syslog-hostname" description:"Client hostname with which the build logs will be sent to the syslog server." default:"atc-syslog-drainer"`
Address string `long:"syslog-address" description:"Remote syslog server address with port (Example: 0.0.0.0:514)."`
Transport string `long:"syslog-transport" description:"Transport protocol for syslog messages (Currently supporting tcp, udp & tls)."`
DrainInterval time.Duration `long:"syslog-drain-interval" description:"Interval over which checking is done for new build logs to send to syslog server (duration measurement units are s/m/h; eg. 30s/30m/1h)" default:"30s"`
CACerts []string `long:"syslog-ca-cert" description:"Paths to PEM-encoded CA cert files to use to verify the Syslog server SSL cert."`
} ` group:"Syslog Drainer Configuration"`
Auth struct {
AuthFlags skycmd.AuthFlags
MainTeamFlags skycmd.AuthTeamFlags `group:"Authentication (Main Team)" namespace:"main-team"`
} `group:"Authentication"`
}
var HelpError = errors.New("must specify one of `--current-db-version`, `--supported-db-version`, or `--migrate-db-to-version`")
type Migration struct {
Postgres flag.PostgresConfig `group:"PostgreSQL Configuration" namespace:"postgres"`
EncryptionKey flag.Cipher `long:"encryption-key" description:"A 16 or 32 length key used to encrypt sensitive information before storing it in the database."`
CurrentDBVersion bool `long:"current-db-version" description:"Print the current database version and exit"`
SupportedDBVersion bool `long:"supported-db-version" description:"Print the max supported database version and exit"`
MigrateDBToVersion int `long:"migrate-db-to-version" description:"Migrate to the specified database version and exit"`
}
func (m *Migration) Execute(args []string) error {
if m.CurrentDBVersion {
return m.currentDBVersion()
}
if m.SupportedDBVersion {
return m.supportedDBVersion()
}
if m.MigrateDBToVersion > 0 {
return m.migrateDBToVersion()
}
return HelpError
}
func (cmd *Migration) currentDBVersion() error {
helper := migration.NewOpenHelper(
defaultDriverName,
cmd.Postgres.ConnectionString(),
nil,
encryption.NewNoEncryption(),
)
version, err := helper.CurrentVersion()
if err != nil {
return err
}
fmt.Println(version)
return nil
}
func (cmd *Migration) supportedDBVersion() error {
helper := migration.NewOpenHelper(
defaultDriverName,
cmd.Postgres.ConnectionString(),
nil,
encryption.NewNoEncryption(),
)
version, err := helper.SupportedVersion()
if err != nil {
return err
}
fmt.Println(version)
return nil
}
func (cmd *Migration) migrateDBToVersion() error {
version := cmd.MigrateDBToVersion
var newKey *encryption.Key
if cmd.EncryptionKey.AEAD != nil {
newKey = encryption.NewKey(cmd.EncryptionKey.AEAD)
}
var strategy encryption.Strategy
if newKey != nil {
strategy = newKey
} else {
strategy = encryption.NewNoEncryption()
}
helper := migration.NewOpenHelper(
defaultDriverName,
cmd.Postgres.ConnectionString(),
nil,
strategy,
)
err := helper.MigrateToVersion(version)
if err != nil {
return fmt.Errorf("Could not migrate to version: %d Reason: %s", version, err.Error())
}
fmt.Println("Successfully migrated to version:", version)
return nil
}
func (cmd *ATCCommand) WireDynamicFlags(commandFlags *flags.Command) {
cmd.RunCommand.WireDynamicFlags(commandFlags)
}
func (cmd *RunCommand) WireDynamicFlags(commandFlags *flags.Command) {
var metricsGroup *flags.Group
var credsGroup *flags.Group
var authGroup *flags.Group
groups := commandFlags.Groups()
for i := 0; i < len(groups); i++ {
group := groups[i]
if credsGroup == nil && group.ShortDescription == "Credential Management" {
credsGroup = group
}
if metricsGroup == nil && group.ShortDescription == "Metrics & Diagnostics" {
metricsGroup = group
}
if authGroup == nil && group.ShortDescription == "Authentication" {
authGroup = group
}
if metricsGroup != nil && credsGroup != nil && authGroup != nil {
break
}
groups = append(groups, group.Groups()...)
}
if metricsGroup == nil {
panic("could not find Metrics & Diagnostics group for registering emitters")
}
if credsGroup == nil {
panic("could not find Credential Management group for registering managers")
}
if authGroup == nil {
panic("could not find Authentication group for registering connectors")
}
managerConfigs := make(creds.Managers)
for name, p := range creds.ManagerFactories() {
managerConfigs[name] = p.AddConfig(credsGroup)
}
cmd.CredentialManagers = managerConfigs
metric.WireEmitters(metricsGroup)
skycmd.WireConnectors(authGroup)
skycmd.WireTeamConnectors(authGroup.Find("Authentication (Main Team)"))
}
func (cmd *RunCommand) Execute(args []string) error {
runner, err := cmd.Runner(args)
if err != nil {
return err
}
return <-ifrit.Invoke(sigmon.New(runner)).Wait()
}
func (cmd *RunCommand) Runner(positionalArguments []string) (ifrit.Runner, error) {
if cmd.ExternalURL.URL == nil {
cmd.ExternalURL = cmd.DefaultURL()
}
if len(positionalArguments) != 0 {
return nil, fmt.Errorf("unexpected positional arguments: %v", positionalArguments)
}
err := cmd.validate()
if err != nil {
return nil, err
}
logger, reconfigurableSink := cmd.Logger.Logger("atc")
commandSession := logger.Session("cmd")
startTime := time.Now()
commandSession.Info("start")
defer commandSession.Info("finish", lager.Data{
"duration": time.Now().Sub(startTime),
})
atc.EnableGlobalResources = cmd.EnableGlobalResources
radar.GlobalResourceCheckTimeout = cmd.GlobalResourceCheckTimeout
//FIXME: These only need to run once for the entire binary. At the moment,
//they rely on state of the command.
db.SetupConnectionRetryingDriver(
"postgres",
cmd.Postgres.ConnectionString(),
retryingDriverName,
)
// Register the sink that collects error metrics
if cmd.Metrics.CaptureErrorMetrics {
errorSinkCollector := metric.NewErrorSinkCollector(logger)
logger.RegisterSink(&errorSinkCollector)
}
http.HandleFunc("/debug/connections", func(w http.ResponseWriter, r *http.Request) {
for _, stack := range db.GlobalConnectionTracker.Current() {
fmt.Fprintln(w, stack)
}
})
if err := cmd.configureMetrics(logger); err != nil {
return nil, err
}
lockConn, err := cmd.constructLockConn(retryingDriverName)
if err != nil {
return nil, err
}
lockFactory := lock.NewLockFactory(lockConn, metric.LogLockAcquired, metric.LogLockReleased)
apiConn, err := cmd.constructDBConn(retryingDriverName, logger, 32, "api", lockFactory)
if err != nil {
return nil, err
}
backendConn, err := cmd.constructDBConn(retryingDriverName, logger, 32, "backend", lockFactory)
if err != nil {
return nil, err
}
storage, err := storage.NewPostgresStorage(logger, cmd.Postgres)
if err != nil {
return nil, err
}
secretManager, err := cmd.secretManager(logger)
if err != nil {
return nil, err
}
members, err := cmd.constructMembers(logger, reconfigurableSink, apiConn, backendConn, storage, lockFactory, secretManager)
if err != nil {
return nil, err
}
members = append(members, grouper.Member{
Name: "periodic-metrics",
Runner: metric.PeriodicallyEmit(
logger.Session("periodic-metrics"),
10*time.Second,
),
})
onReady := func() {
logData := lager.Data{
"http": cmd.nonTLSBindAddr(),
"debug": cmd.debugBindAddr(),
}
if cmd.isTLSEnabled() {
logData["https"] = cmd.tlsBindAddr()
}
logger.Info("listening", logData)
}
onExit := func() {
for _, closer := range []Closer{lockConn, apiConn, backendConn, storage} {
closer.Close()
}
}
return run(grouper.NewParallel(os.Interrupt, members), onReady, onExit), nil
}
func (cmd *RunCommand) constructMembers(
logger lager.Logger,
reconfigurableSink *lager.ReconfigurableSink,
apiConn db.Conn,
backendConn db.Conn,
storage storage.Storage,
lockFactory lock.LockFactory,
secretManager creds.Secrets,
) ([]grouper.Member, error) {
if cmd.TelemetryOptIn {
url := fmt.Sprintf("http://telemetry.concourse-ci.org/?version=%s", concourse.Version)
go func() {
_, err := http.Get(url)
if err != nil {
logger.Error("telemetry-version", err)
}
}()
}
apiMembers, err := cmd.constructAPIMembers(logger, reconfigurableSink, apiConn, storage, lockFactory, secretManager)
if err != nil {
return nil, err
}
backendMembers, err := cmd.constructBackendMembers(logger, backendConn, lockFactory, secretManager)
if err != nil {
return nil, err
}
return append(apiMembers, backendMembers...), nil
}
func (cmd *RunCommand) constructAPIMembers(
logger lager.Logger,
reconfigurableSink *lager.ReconfigurableSink,
dbConn db.Conn,
storage storage.Storage,
lockFactory lock.LockFactory,
secretManager creds.Secrets,
) ([]grouper.Member, error) {
teamFactory := db.NewTeamFactory(dbConn, lockFactory)
_, err := teamFactory.CreateDefaultTeamIfNotExists()
if err != nil {
return nil, err
}
err = cmd.configureAuthForDefaultTeam(teamFactory)
if err != nil {
return nil, err
}
httpClient, err := cmd.skyHttpClient()
if err != nil {
return nil, err
}
authHandler, err := skymarshal.NewServer(&skymarshal.Config{
Logger: logger,
TeamFactory: teamFactory,
Flags: cmd.Auth.AuthFlags,
ExternalURL: cmd.ExternalURL.String(),
HTTPClient: httpClient,
Storage: storage,
})
if err != nil {
return nil, err
}
resourceFactory := resource.NewResourceFactory()
dbResourceCacheFactory := db.NewResourceCacheFactory(dbConn, lockFactory)
fetchSourceFactory := resource.NewFetchSourceFactory(dbResourceCacheFactory, resourceFactory)
resourceFetcher := resource.NewFetcher(clock.NewClock(), lockFactory, fetchSourceFactory)
dbResourceConfigFactory := db.NewResourceConfigFactory(dbConn, lockFactory)
imageResourceFetcherFactory := image.NewImageResourceFetcherFactory(
dbResourceCacheFactory,
dbResourceConfigFactory,
resourceFetcher,
resourceFactory,
)
dbWorkerBaseResourceTypeFactory := db.NewWorkerBaseResourceTypeFactory(dbConn)
dbWorkerTaskCacheFactory := db.NewWorkerTaskCacheFactory(dbConn)
dbTaskCacheFactory := db.NewTaskCacheFactory(dbConn)
dbVolumeRepository := db.NewVolumeRepository(dbConn)
dbWorkerFactory := db.NewWorkerFactory(dbConn)
workerVersion, err := workerVersion()
if err != nil {
return nil, err
}
workerProvider := worker.NewDBWorkerProvider(
lockFactory,
retryhttp.NewExponentialBackOffFactory(5*time.Minute),
image.NewImageFactory(imageResourceFetcherFactory),
dbResourceCacheFactory,
dbResourceConfigFactory,
dbWorkerBaseResourceTypeFactory,
dbTaskCacheFactory,
dbWorkerTaskCacheFactory,
dbVolumeRepository,
teamFactory,
dbWorkerFactory,
workerVersion,
cmd.BaggageclaimResponseHeaderTimeout,
)
pool := worker.NewPool(clock.NewClock(), lockFactory, workerProvider)
workerClient := worker.NewClient(pool, workerProvider)
checkContainerStrategy := worker.NewRandomPlacementStrategy()
radarScannerFactory := radar.NewScannerFactory(
pool,
resourceFactory,
dbResourceConfigFactory,
cmd.ResourceTypeCheckingInterval,
cmd.ResourceCheckingInterval,
cmd.ExternalURL.String(),
secretManager,
checkContainerStrategy,
)
credsManagers := cmd.CredentialManagers
dbPipelineFactory := db.NewPipelineFactory(dbConn, lockFactory)
dbJobFactory := db.NewJobFactory(dbConn, lockFactory)
dbResourceFactory := db.NewResourceFactory(dbConn, lockFactory)
dbContainerRepository := db.NewContainerRepository(dbConn)
gcContainerDestroyer := gc.NewDestroyer(logger, dbContainerRepository, dbVolumeRepository)
dbBuildFactory := db.NewBuildFactory(dbConn, lockFactory, cmd.GC.OneOffBuildGracePeriod)
accessFactory := accessor.NewAccessFactory(authHandler.PublicKey())
apiHandler, err := cmd.constructAPIHandler(
logger,
reconfigurableSink,
teamFactory,
dbPipelineFactory,
dbJobFactory,
dbResourceFactory,
dbWorkerFactory,
dbVolumeRepository,
dbContainerRepository,
gcContainerDestroyer,
dbBuildFactory,
dbResourceConfigFactory,
workerClient,
radarScannerFactory,
secretManager,
credsManagers,
accessFactory,
)
if err != nil {
return nil, err
}
webHandler, err := webHandler(logger)
if err != nil {
return nil, err
}
var httpHandler, httpsHandler http.Handler
if cmd.isTLSEnabled() {
httpHandler = cmd.constructHTTPHandler(
logger,
tlsRedirectHandler{
matchHostname: cmd.ExternalURL.URL.Hostname(),
externalHost: cmd.ExternalURL.URL.Host,
baseHandler: webHandler,
},
// note: intentionally not wrapping API; redirecting is more trouble than
// it's worth.
// we're mainly interested in having the web UI consistently https:// -
// API requests will likely not respect the redirected https:// URI upon
// the next request, plus the payload will have already been sent in
// plaintext
apiHandler,
tlsRedirectHandler{
matchHostname: cmd.ExternalURL.URL.Hostname(),
externalHost: cmd.ExternalURL.URL.Host,
baseHandler: authHandler,
},
)
httpsHandler = cmd.constructHTTPHandler(
logger,
webHandler,
apiHandler,
authHandler,
)
} else {
httpHandler = cmd.constructHTTPHandler(
logger,
webHandler,
apiHandler,
authHandler,
)
}
members := []grouper.Member{
{Name: "debug", Runner: http_server.New(
cmd.debugBindAddr(),
http.DefaultServeMux,
)},
{Name: "web", Runner: http_server.New(
cmd.nonTLSBindAddr(),
httpHandler,
)},
}
if httpsHandler != nil {
tlsConfig, err := cmd.tlsConfig(logger, dbConn)
if err != nil {
return nil, err
}
members = append(members, grouper.Member{Name: "web-tls", Runner: http_server.NewTLSServer(
cmd.tlsBindAddr(),
httpsHandler,
tlsConfig,
)})
}
return members, nil
}
func (cmd *RunCommand) constructBackendMembers(
logger lager.Logger,
dbConn db.Conn,
lockFactory lock.LockFactory,
secretManager creds.Secrets,
) ([]grouper.Member, error) {
if cmd.Syslog.Address != "" && cmd.Syslog.Transport == "" {
return nil, fmt.Errorf("syslog Drainer is misconfigured, cannot configure a drainer without a transport")
}
syslogDrainConfigured := true
if cmd.Syslog.Address == "" {
syslogDrainConfigured = false
}
teamFactory := db.NewTeamFactory(dbConn, lockFactory)
resourceFactory := resource.NewResourceFactory()
dbResourceCacheFactory := db.NewResourceCacheFactory(dbConn, lockFactory)
fetchSourceFactory := resource.NewFetchSourceFactory(dbResourceCacheFactory, resourceFactory)
resourceFetcher := resource.NewFetcher(clock.NewClock(), lockFactory, fetchSourceFactory)
dbResourceConfigFactory := db.NewResourceConfigFactory(dbConn, lockFactory)
imageResourceFetcherFactory := image.NewImageResourceFetcherFactory(
dbResourceCacheFactory,
dbResourceConfigFactory,
resourceFetcher,
resourceFactory,
)
dbWorkerBaseResourceTypeFactory := db.NewWorkerBaseResourceTypeFactory(dbConn)
dbTaskCacheFactory := db.NewTaskCacheFactory(dbConn)
dbWorkerTaskCacheFactory := db.NewWorkerTaskCacheFactory(dbConn)
dbVolumeRepository := db.NewVolumeRepository(dbConn)
dbWorkerFactory := db.NewWorkerFactory(dbConn)
workerVersion, err := workerVersion()
if err != nil {
return nil, err
}
workerProvider := worker.NewDBWorkerProvider(
lockFactory,
retryhttp.NewExponentialBackOffFactory(5*time.Minute),
image.NewImageFactory(imageResourceFetcherFactory),
dbResourceCacheFactory,
dbResourceConfigFactory,
dbWorkerBaseResourceTypeFactory,
dbTaskCacheFactory,
dbWorkerTaskCacheFactory,
dbVolumeRepository,
teamFactory,
dbWorkerFactory,
workerVersion,
cmd.BaggageclaimResponseHeaderTimeout,
)
pool := worker.NewPool(clock.NewClock(), lockFactory, workerProvider)
workerClient := worker.NewClient(pool, workerProvider)
defaultLimits, err := cmd.parseDefaultLimits()
if err != nil {
return nil, err
}
buildContainerStrategy := cmd.chooseBuildContainerStrategy()
checkContainerStrategy := worker.NewRandomPlacementStrategy()
engine := cmd.constructEngine(
pool,
workerClient,
resourceFetcher,
dbResourceCacheFactory,
dbResourceConfigFactory,
secretManager,
defaultLimits,
buildContainerStrategy,
resourceFactory,
)
radarSchedulerFactory := pipelines.NewRadarSchedulerFactory(
pool,
resourceFactory,
dbResourceConfigFactory,
cmd.ResourceTypeCheckingInterval,
cmd.ResourceCheckingInterval,
checkContainerStrategy,
)
dbWorkerLifecycle := db.NewWorkerLifecycle(dbConn)
dbResourceCacheLifecycle := db.NewResourceCacheLifecycle(dbConn)
dbContainerRepository := db.NewContainerRepository(dbConn)
dbArtifactLifecycle := db.NewArtifactLifecycle(dbConn)
resourceConfigCheckSessionLifecycle := db.NewResourceConfigCheckSessionLifecycle(dbConn)
dbBuildFactory := db.NewBuildFactory(dbConn, lockFactory, cmd.GC.OneOffBuildGracePeriod)
bus := dbConn.Bus()
dbPipelineFactory := db.NewPipelineFactory(dbConn, lockFactory)
members := []grouper.Member{
{Name: "pipelines", Runner: pipelines.SyncRunner{
Syncer: cmd.constructPipelineSyncer(
logger.Session("pipelines"),
dbPipelineFactory,
radarSchedulerFactory,
secretManager,
bus,
),
Interval: 10 * time.Second,
Clock: clock.NewClock(),
}},
{Name: "builds", Runner: builds.TrackerRunner{
Tracker: builds.NewTracker(
logger.Session("build-tracker"),
dbBuildFactory,
engine,
),
Notifications: bus,
Interval: cmd.BuildTrackerInterval,
Clock: clock.NewClock(),
Logger: logger.Session("tracker-runner"),
}},
{Name: "collector", Runner: lockrunner.NewRunner(
logger.Session("collector"),
gc.NewCollector(
gc.NewBuildCollector(dbBuildFactory),
gc.NewWorkerCollector(dbWorkerLifecycle),
gc.NewResourceCacheUseCollector(dbResourceCacheLifecycle),
gc.NewResourceConfigCollector(dbResourceConfigFactory),
gc.NewResourceCacheCollector(dbResourceCacheLifecycle),
gc.NewArtifactCollector(dbArtifactLifecycle),
gc.NewVolumeCollector(
dbVolumeRepository,
cmd.GC.MissingGracePeriod,
),
gc.NewContainerCollector(
dbContainerRepository,
gc.NewWorkerJobRunner(
logger.Session("container-collector-worker-job-runner"),
workerProvider,
time.Minute,
),
cmd.GC.MissingGracePeriod,
),
gc.NewResourceConfigCheckSessionCollector(
resourceConfigCheckSessionLifecycle,
),
),
"collector",
lockFactory,
clock.NewClock(),
cmd.GC.Interval,
)},
// run separately so as to not preempt critical GC
{Name: "build-log-collector", Runner: lockrunner.NewRunner(
logger.Session("build-log-collector"),
gc.NewBuildLogCollector(
dbPipelineFactory,
500,
gc.NewBuildLogRetentionCalculator(
cmd.DefaultBuildLogsToRetain,
cmd.MaxBuildLogsToRetain,
cmd.DefaultDaysToRetainBuildLogs,
cmd.MaxDaysToRetainBuildLogs,
),
syslogDrainConfigured,
),
"build-reaper",
lockFactory,
clock.NewClock(),
30*time.Second,
)},
}
//Syslog Drainer Configuration
if syslogDrainConfigured {
members = append(members, grouper.Member{
Name: "syslog", Runner: lockrunner.NewRunner(
logger.Session("syslog"),
syslog.NewDrainer(
cmd.Syslog.Transport,
cmd.Syslog.Address,
cmd.Syslog.Hostname,
cmd.Syslog.CACerts,
dbBuildFactory,
),
"syslog-drainer",
lockFactory,
clock.NewClock(),
cmd.Syslog.DrainInterval,
)},
)
}
if cmd.Worker.GardenURL.URL != nil {
members = cmd.appendStaticWorker(logger, dbWorkerFactory, members)
}
return members, nil
}
func workerVersion() (version.Version, error) {
return version.NewVersionFromString(concourse.WorkerVersion)
}
func (cmd *RunCommand) secretManager(logger lager.Logger) (creds.Secrets, error) {
var secretsFactory creds.SecretsFactory = noop.NewNoopFactory()
for name, manager := range cmd.CredentialManagers {
if !manager.IsConfigured() {
continue
}
credsLogger := logger.Session("credential-manager", lager.Data{
"name": name,
})
credsLogger.Info("configured credentials manager")
err := manager.Init(credsLogger)
if err != nil {
return nil, err
}
err = manager.Validate()
if err != nil {
return nil, fmt.Errorf("credential manager '%s' misconfigured: %s", name, err)
}
secretsFactory, err = manager.NewSecretsFactory(credsLogger)
if err != nil {
return nil, err
}
break
}
result := secretsFactory.NewSecrets()
result = creds.NewRetryableSecrets(result, cmd.CredentialManagement.RetryConfig)
if cmd.CredentialManagement.CacheConfig.Enabled {
result = creds.NewCachedSecrets(result, cmd.CredentialManagement.CacheConfig)
}
return result, nil
}
func (cmd *RunCommand) newKey() *encryption.Key {
var newKey *encryption.Key
if cmd.EncryptionKey.AEAD != nil {
newKey = encryption.NewKey(cmd.EncryptionKey.AEAD)
}
return newKey
}
func (cmd *RunCommand) oldKey() *encryption.Key {
var oldKey *encryption.Key
if cmd.OldEncryptionKey.AEAD != nil {
oldKey = encryption.NewKey(cmd.OldEncryptionKey.AEAD)
}
return oldKey
}
func webHandler(logger lager.Logger) (http.Handler, error) {
webHandler, err := web.NewHandler(logger)
if err != nil {
return nil, err
}
return metric.WrapHandler(logger, "web", webHandler), nil
}
func (cmd *RunCommand) skyHttpClient() (*http.Client, error) {
httpClient := http.DefaultClient
if cmd.isTLSEnabled() {
certpool, err := x509.SystemCertPool()
if err != nil {
return nil, err
}
if !cmd.LetsEncrypt.Enable {
cert, err := tls.LoadX509KeyPair(string(cmd.TLSCert), string(cmd.TLSKey))
if err != nil {
return nil, err
}
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return nil, err
}
certpool.AddCert(x509Cert)
}
httpClient.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: certpool,
},
}
} else {
httpClient.Transport = http.DefaultTransport
}
httpClient.Transport = mitmRoundTripper{
RoundTripper: httpClient.Transport,
SourceHost: cmd.ExternalURL.URL.Host,
TargetURL: cmd.DefaultURL().URL,
}
return httpClient, nil
}
type mitmRoundTripper struct {
http.RoundTripper
SourceHost string
TargetURL *url.URL
}
func (tripper mitmRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
if req.URL.Host == tripper.SourceHost {
req.URL.Scheme = tripper.TargetURL.Scheme
req.URL.Host = tripper.TargetURL.Host
}
return tripper.RoundTripper.RoundTrip(req)
}
func (cmd *RunCommand) tlsConfig(logger lager.Logger, dbConn db.Conn) (*tls.Config, error) {
var tlsConfig *tls.Config
tlsConfig = atc.DefaultTLSConfig()
if cmd.isTLSEnabled() {
tlsLogger := logger.Session("tls-enabled")
if cmd.LetsEncrypt.Enable {
tlsLogger.Debug("using-autocert-manager")
cache, err := newDbCache(dbConn)
if err != nil {
return nil, err
}
m := autocert.Manager{
Prompt: autocert.AcceptTOS,
Cache: cache,
HostPolicy: autocert.HostWhitelist(cmd.ExternalURL.URL.Hostname()),
Client: &acme.Client{DirectoryURL: cmd.LetsEncrypt.ACMEURL.String()},
}
tlsConfig.NextProtos = append(tlsConfig.NextProtos, acme.ALPNProto)
tlsConfig.GetCertificate = m.GetCertificate
} else {
tlsLogger.Debug("loading-tls-certs")
cert, err := tls.LoadX509KeyPair(string(cmd.TLSCert), string(cmd.TLSKey))
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{cert}
}
}
return tlsConfig, nil
}
func (cmd *RunCommand) parseDefaultLimits() (atc.ContainerLimits, error) {
return atc.ContainerLimitsParser(map[string]interface{}{
"cpu": cmd.DefaultCpuLimit,
"memory": cmd.DefaultMemoryLimit,
})
}
func (cmd *RunCommand) defaultBindIP() net.IP {
URL := cmd.BindIP.String()
if URL == "0.0.0.0" {
URL = "127.0.0.1"
}
return net.ParseIP(URL)
}
func (cmd *RunCommand) DefaultURL() flag.URL {
return flag.URL{
URL: &url.URL{
Scheme: "http",
Host: fmt.Sprintf("%s:%d", cmd.defaultBindIP().String(), cmd.BindPort),
},
}
}
func run(runner ifrit.Runner, onReady func(), onExit func()) ifrit.Runner {
return ifrit.RunFunc(func(signals <-chan os.Signal, ready chan<- struct{}) error {
process := ifrit.Background(runner)
subExited := process.Wait()
subReady := process.Ready()
for {
select {
case <-subReady:
onReady()
close(ready)
subReady = nil
case err := <-subExited:
onExit()
return err
case sig := <-signals:
process.Signal(sig)
}
}
})
}
func (cmd *RunCommand) validate() error {
var errs *multierror.Error
switch {
case cmd.TLSBindPort == 0:
if cmd.TLSCert != "" || cmd.TLSKey != "" || cmd.LetsEncrypt.Enable {
errs = multierror.Append(
errs,
errors.New("must specify --tls-bind-port to use TLS"),
)
}
case cmd.LetsEncrypt.Enable:
if cmd.TLSCert != "" || cmd.TLSKey != "" {
errs = multierror.Append(
errs,
errors.New("cannot specify --enable-lets-encrypt if --tls-cert or --tls-key are set"),
)
}
case cmd.TLSCert != "" && cmd.TLSKey != "":
if cmd.ExternalURL.URL.Scheme != "https" {
errs = multierror.Append(
errs,
errors.New("must specify HTTPS external-url to use TLS"),
)
}
default:
errs = multierror.Append(
errs,
errors.New("must specify --tls-cert and --tls-key, or --enable-lets-encrypt to use TLS"),
)
}
return errs.ErrorOrNil()
}
func (cmd *RunCommand) nonTLSBindAddr() string {
return fmt.Sprintf("%s:%d", cmd.BindIP, cmd.BindPort)
}
func (cmd *RunCommand) tlsBindAddr() string {
return fmt.Sprintf("%s:%d", cmd.BindIP, cmd.TLSBindPort)
}
func (cmd *RunCommand) debugBindAddr() string {
return fmt.Sprintf("%s:%d", cmd.DebugBindIP, cmd.DebugBindPort)
}
func (cmd *RunCommand) configureMetrics(logger lager.Logger) error {
host := cmd.Metrics.HostName
if host == "" {
host, _ = os.Hostname()
}
return metric.Initialize(logger.Session("metrics"), host, cmd.Metrics.Attributes)
}
func (cmd *RunCommand) constructDBConn(
driverName string,
logger lager.Logger,
maxConn int,
connectionName string,
lockFactory lock.LockFactory,
) (db.Conn, error) {
dbConn, err := db.Open(logger.Session("db"), driverName, cmd.Postgres.ConnectionString(), cmd.newKey(), cmd.oldKey(), connectionName, lockFactory)
if err != nil {
return nil, fmt.Errorf("failed to migrate database: %s", err)
}
// Instrument with Metrics
dbConn = metric.CountQueries(dbConn)
metric.Databases = append(metric.Databases, dbConn)
// Instrument with Logging
if cmd.LogDBQueries {
dbConn = db.Log(logger.Session("log-conn"), dbConn)
}
// Prepare
dbConn.SetMaxOpenConns(maxConn)
return dbConn, nil
}
type Closer interface {
Close() error
}
func (cmd *RunCommand) constructLockConn(driverName string) (*sql.DB, error) {
dbConn, err := sql.Open(driverName, cmd.Postgres.ConnectionString())
if err != nil {
return nil, err
}
dbConn.SetMaxOpenConns(1)
dbConn.SetMaxIdleConns(1)
dbConn.SetConnMaxLifetime(0)
return dbConn, nil
}
func (cmd *RunCommand) chooseBuildContainerStrategy() worker.ContainerPlacementStrategy {
var strategy worker.ContainerPlacementStrategy
switch cmd.ContainerPlacementStrategy {
case "random":
strategy = worker.NewRandomPlacementStrategy()
case "fewest-build-containers":
strategy = worker.NewFewestBuildContainersPlacementStrategy()
default:
strategy = worker.NewVolumeLocalityPlacementStrategy()
}
return strategy
}
func (cmd *RunCommand) configureAuthForDefaultTeam(teamFactory db.TeamFactory) error {
team, found, err := teamFactory.FindTeam(atc.DefaultTeamName)
if err != nil {
return err
}
if !found {
return errors.New("default team not found")
}
auth, err := cmd.Auth.MainTeamFlags.Format()
if err != nil {
return fmt.Errorf("default team auth not configured: %v", err)
}
err = team.UpdateProviderAuth(atc.TeamAuth(auth))
if err != nil {
return err
}
return nil
}
func (cmd *RunCommand) constructEngine(
workerPool worker.Pool,
workerClient worker.Client,
resourceFetcher resource.Fetcher,
resourceCacheFactory db.ResourceCacheFactory,
resourceConfigFactory db.ResourceConfigFactory,
secretManager creds.Secrets,
defaultLimits atc.ContainerLimits,
strategy worker.ContainerPlacementStrategy,
resourceFactory resource.ResourceFactory,
) engine.Engine {
stepFactory := builder.NewStepFactory(
workerPool,
workerClient,
resourceFetcher,
resourceCacheFactory,
resourceConfigFactory,
secretManager,
defaultLimits,
strategy,
resourceFactory,
)
stepBuilder := builder.NewStepBuilder(
stepFactory,
builder.NewDelegateFactory(),
cmd.ExternalURL.String(),
)
return engine.NewEngine(stepBuilder)
}
func (cmd *RunCommand) constructHTTPHandler(
logger lager.Logger,
webHandler http.Handler,
apiHandler http.Handler,
authHandler http.Handler,
) http.Handler {
webMux := http.NewServeMux()
webMux.Handle("/api/v1/", apiHandler)
webMux.Handle("/sky/", authHandler)
webMux.Handle("/auth/", authHandler)
webMux.Handle("/login", authHandler)
webMux.Handle("/logout", authHandler)
webMux.Handle("/", webHandler)
httpHandler := wrappa.LoggerHandler{
Logger: logger,
Handler: wrappa.SecurityHandler{
XFrameOptions: cmd.Server.XFrameOptions,
// proxy Authorization header to/from auth cookie,
// to support auth from JS (EventSource) and custom JWT auth
Handler: auth.CookieSetHandler{
Handler: webMux,
},
},
}
return httpHandler
}
func (cmd *RunCommand) constructAPIHandler(
logger lager.Logger,
reconfigurableSink *lager.ReconfigurableSink,
teamFactory db.TeamFactory,
dbPipelineFactory db.PipelineFactory,
dbJobFactory db.JobFactory,
dbResourceFactory db.ResourceFactory,
dbWorkerFactory db.WorkerFactory,
dbVolumeRepository db.VolumeRepository,
dbContainerRepository db.ContainerRepository,
gcContainerDestroyer gc.Destroyer,
dbBuildFactory db.BuildFactory,
resourceConfigFactory db.ResourceConfigFactory,
workerClient worker.Client,
radarScannerFactory radar.ScannerFactory,
secretManager creds.Secrets,
credsManagers creds.Managers,
accessFactory accessor.AccessFactory,
) (http.Handler, error) {
checkPipelineAccessHandlerFactory := auth.NewCheckPipelineAccessHandlerFactory(teamFactory)
checkBuildReadAccessHandlerFactory := auth.NewCheckBuildReadAccessHandlerFactory(dbBuildFactory)
checkBuildWriteAccessHandlerFactory := auth.NewCheckBuildWriteAccessHandlerFactory(dbBuildFactory)
checkWorkerTeamAccessHandlerFactory := auth.NewCheckWorkerTeamAccessHandlerFactory(dbWorkerFactory)
aud := auditor.NewAuditor(
cmd.Auditor.EnableBuildAuditLog,
cmd.Auditor.EnableContainerAuditLog,
cmd.Auditor.EnableJobAuditLog,
cmd.Auditor.EnablePipelineAuditLog,
cmd.Auditor.EnableResourceAuditLog,
cmd.Auditor.EnableSystemAuditLog,
cmd.Auditor.EnableTeamAuditLog,
cmd.Auditor.EnableWorkerAuditLog,
cmd.Auditor.EnableVolumeAuditLog,
logger,
)
apiWrapper := wrappa.MultiWrappa{
wrappa.NewAPIMetricsWrappa(logger),
wrappa.NewAPIAuthWrappa(
checkPipelineAccessHandlerFactory,
checkBuildReadAccessHandlerFactory,
checkBuildWriteAccessHandlerFactory,
checkWorkerTeamAccessHandlerFactory,
),
wrappa.NewConcourseVersionWrappa(concourse.Version),
wrappa.NewAccessorWrappa(accessFactory, aud),
}
return api.NewHandler(
logger,
cmd.ExternalURL.String(),
cmd.Server.ClusterName,
apiWrapper,
teamFactory,
dbPipelineFactory,
dbJobFactory,
dbResourceFactory,
dbWorkerFactory,
dbVolumeRepository,
dbContainerRepository,
gcContainerDestroyer,
dbBuildFactory,
resourceConfigFactory,
buildserver.NewEventHandler,
workerClient,
radarScannerFactory,
reconfigurableSink,
cmd.isTLSEnabled(),
cmd.CLIArtifactsDir.Path(),
concourse.Version,
concourse.WorkerVersion,
secretManager,
credsManagers,
containerserver.NewInterceptTimeoutFactory(cmd.InterceptIdleTimeout),
)
}
type tlsRedirectHandler struct {
matchHostname string
externalHost string
baseHandler http.Handler
}
func (h tlsRedirectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.Host, h.matchHostname) && (r.Method == "GET" || r.Method == "HEAD") {
u := url.URL{
Scheme: "https",
Host: h.externalHost,
Path: r.URL.Path,
RawQuery: r.URL.RawQuery,
}
http.Redirect(w, r, u.String(), http.StatusMovedPermanently)
} else {
h.baseHandler.ServeHTTP(w, r)
}
}
func (cmd *RunCommand) constructPipelineSyncer(
logger lager.Logger,
pipelineFactory db.PipelineFactory,
radarSchedulerFactory pipelines.RadarSchedulerFactory,
secretManager creds.Secrets,
bus db.NotificationsBus,
) *pipelines.Syncer {
return pipelines.NewSyncer(
logger,
pipelineFactory,
func(pipeline db.Pipeline) ifrit.Runner {
variables := creds.NewVariables(secretManager, pipeline.TeamName(), pipeline.Name())
return grouper.NewParallel(os.Interrupt, grouper.Members{
{
Name: fmt.Sprintf("radar:%d", pipeline.ID()),
Runner: radar.NewRunner(
logger.Session("radar").WithData(lager.Data{
"team": pipeline.TeamName(),
"pipeline": pipeline.Name(),
}),
cmd.Developer.Noop,
radarSchedulerFactory.BuildScanRunnerFactory(pipeline, cmd.ExternalURL.String(), variables, bus),
pipeline,
1*time.Minute,
),
},
{
Name: fmt.Sprintf("scheduler:%d", pipeline.ID()),
Runner: &scheduler.Runner{
Logger: logger.Session("scheduler", lager.Data{
"team": pipeline.TeamName(),
"pipeline": pipeline.Name(),
}),
Pipeline: pipeline,
Scheduler: radarSchedulerFactory.BuildScheduler(pipeline),
Noop: cmd.Developer.Noop,
Interval: 10 * time.Second,
},
},
})
},
)
}
func (cmd *RunCommand) appendStaticWorker(
logger lager.Logger,
workerFactory db.WorkerFactory,
members []grouper.Member,
) []grouper.Member {
var resourceTypes []atc.WorkerResourceType
for t, resourcePath := range cmd.Worker.ResourceTypes {
resourceTypes = append(resourceTypes, atc.WorkerResourceType{
Type: t,
Image: resourcePath,
})
}
return append(members,
grouper.Member{
Name: "static-worker",
Runner: worker.NewHardcoded(
logger,
workerFactory,
clock.NewClock(),
cmd.Worker.GardenURL.URL.Host,
cmd.Worker.BaggageclaimURL.String(),
resourceTypes,
),
},
)
}
func (cmd *RunCommand) isTLSEnabled() bool {
return cmd.TLSBindPort != 0
}