2022-03-22 19:17:50 +00:00
package cli
import (
"context"
2022-03-24 19:21:05 +00:00
"crypto/tls"
"crypto/x509"
2022-03-24 15:07:33 +00:00
"database/sql"
2022-03-24 19:21:05 +00:00
"encoding/pem"
2022-04-11 23:54:30 +00:00
"errors"
2022-03-22 19:17:50 +00:00
"fmt"
2022-05-02 01:31:12 +00:00
"io"
"log"
2022-03-22 19:17:50 +00:00
"net"
"net/http"
2022-05-03 12:48:02 +00:00
"net/http/pprof"
2022-03-22 19:17:50 +00:00
"net/url"
"os"
2022-03-24 15:07:33 +00:00
"os/signal"
2022-06-15 21:02:18 +00:00
"os/user"
2022-03-28 19:57:19 +00:00
"path/filepath"
2022-06-15 21:02:18 +00:00
"strconv"
"strings"
2022-07-27 15:21:21 +00:00
"sync"
2022-03-22 19:17:50 +00:00
"time"
2022-08-01 04:05:35 +00:00
"github.com/coreos/go-oidc/v3/oidc"
2022-03-29 00:19:28 +00:00
"github.com/coreos/go-systemd/daemon"
2022-06-15 21:02:18 +00:00
embeddedpostgres "github.com/fergusstrange/embedded-postgres"
2022-04-23 22:58:57 +00:00
"github.com/google/go-github/v43/github"
2022-06-17 05:26:40 +00:00
"github.com/google/uuid"
2022-04-18 22:40:25 +00:00
"github.com/pion/turn/v2"
2022-04-25 04:52:07 +00:00
"github.com/pion/webrtc/v3"
2022-08-08 15:09:46 +00:00
"github.com/prometheus/client_golang/prometheus"
2022-05-03 12:48:02 +00:00
"github.com/prometheus/client_golang/prometheus/promhttp"
2022-06-16 15:09:22 +00:00
"github.com/spf13/afero"
2022-03-29 00:19:28 +00:00
"github.com/spf13/cobra"
2022-05-20 15:51:06 +00:00
sdktrace "go.opentelemetry.io/otel/sdk/trace"
2022-04-23 22:58:57 +00:00
"golang.org/x/oauth2"
xgithub "golang.org/x/oauth2/github"
2022-06-10 18:38:11 +00:00
"golang.org/x/sync/errgroup"
2022-03-29 00:19:28 +00:00
"golang.org/x/xerrors"
"google.golang.org/api/idtoken"
"google.golang.org/api/option"
2022-05-19 22:43:07 +00:00
2022-03-22 19:17:50 +00:00
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
2022-07-14 21:51:44 +00:00
"github.com/coder/coder/buildinfo"
2022-03-28 19:26:41 +00:00
"github.com/coder/coder/cli/cliflag"
2022-03-22 19:17:50 +00:00
"github.com/coder/coder/cli/cliui"
2022-03-24 15:07:33 +00:00
"github.com/coder/coder/cli/config"
2022-03-22 19:17:50 +00:00
"github.com/coder/coder/coderd"
2022-05-11 22:03:02 +00:00
"github.com/coder/coder/coderd/autobuild/executor"
2022-03-25 21:07:45 +00:00
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/databasefake"
2022-04-14 15:29:40 +00:00
"github.com/coder/coder/coderd/devtunnel"
2022-04-06 00:18:26 +00:00
"github.com/coder/coder/coderd/gitsshkey"
2022-08-08 15:09:46 +00:00
"github.com/coder/coder/coderd/prometheusmetrics"
2022-06-17 05:26:40 +00:00
"github.com/coder/coder/coderd/telemetry"
2022-05-19 22:43:07 +00:00
"github.com/coder/coder/coderd/tracing"
2022-04-18 22:40:25 +00:00
"github.com/coder/coder/coderd/turnconn"
2022-03-22 19:17:50 +00:00
"github.com/coder/coder/codersdk"
2022-07-14 21:51:44 +00:00
"github.com/coder/coder/cryptorand"
"github.com/coder/coder/provisioner/echo"
2022-03-22 19:17:50 +00:00
"github.com/coder/coder/provisioner/terraform"
"github.com/coder/coder/provisionerd"
"github.com/coder/coder/provisionersdk"
"github.com/coder/coder/provisionersdk/proto"
)
2022-04-25 04:52:07 +00:00
// nolint:gocyclo
2022-04-23 17:19:20 +00:00
func server ( ) * cobra . Command {
2022-03-22 19:17:50 +00:00
var (
2022-05-20 10:57:02 +00:00
accessURL string
address string
autobuildPollInterval time . Duration
promEnabled bool
promAddress string
pprofEnabled bool
pprofAddress string
cacheDir string
2022-06-15 21:02:18 +00:00
inMemoryDatabase bool
2022-03-28 19:26:41 +00:00
// provisionerDaemonCount is a uint8 to ensure a number > 0.
2022-04-23 22:58:57 +00:00
provisionerDaemonCount uint8
2022-06-15 21:02:18 +00:00
postgresURL string
2022-04-23 22:58:57 +00:00
oauth2GithubClientID string
oauth2GithubClientSecret string
oauth2GithubAllowedOrganizations [ ] string
2022-07-09 02:37:18 +00:00
oauth2GithubAllowedTeams [ ] string
2022-04-23 22:58:57 +00:00
oauth2GithubAllowSignups bool
2022-08-09 01:49:51 +00:00
oauth2GithubEnterpriseBaseURL string
2022-08-01 04:05:35 +00:00
oidcAllowSignups bool
oidcClientID string
oidcClientSecret string
oidcEmailDomain string
oidcIssuerURL string
oidcScopes [ ] string
2022-06-17 14:02:44 +00:00
telemetryEnable bool
2022-06-17 05:26:40 +00:00
telemetryURL string
2022-04-23 22:58:57 +00:00
tlsCertFile string
tlsClientCAFile string
tlsClientAuth string
tlsEnable bool
tlsKeyFile string
tlsMinVersion string
turnRelayAddress string
2022-05-12 17:37:51 +00:00
tunnel bool
2022-04-25 04:52:07 +00:00
stunServers [ ] string
2022-05-19 22:43:07 +00:00
trace bool
2022-04-23 22:58:57 +00:00
secureAuthCookie bool
sshKeygenAlgorithmRaw string
spooky bool
2022-05-03 21:13:30 +00:00
verbose bool
2022-03-22 19:17:50 +00:00
)
2022-04-18 22:40:25 +00:00
2022-03-22 19:17:50 +00:00
root := & cobra . Command {
2022-05-09 22:42:02 +00:00
Use : "server" ,
Short : "Start a Coder server" ,
2022-03-22 19:17:50 +00:00
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
2022-06-15 21:02:18 +00:00
printLogo ( cmd , spooky )
2022-07-26 14:23:32 +00:00
logger := slog . Make ( sloghuman . Sink ( cmd . ErrOrStderr ( ) ) )
2022-06-15 21:02:18 +00:00
if verbose {
2022-05-03 21:13:30 +00:00
logger = logger . Leveled ( slog . LevelDebug )
}
2022-07-27 15:21:21 +00:00
// Main command context for managing cancellation
// of running services.
ctx , cancel := context . WithCancel ( cmd . Context ( ) )
defer cancel ( )
2022-08-18 13:25:32 +00:00
// Register signals early on so that graceful shutdown can't
// be interrupted by additional signals. Note that we avoid
// shadowing cancel() (from above) here because notifyStop()
// restores default behavior for the signals. This protects
// the shutdown sequence from abrubtly terminating things
// like: database migrations, provisioner work, workspace
// cleanup in dev-mode, etc.
//
// To get out of a graceful shutdown, the user can send
// SIGQUIT with ctrl+\ or SIGKILL with `kill -9`.
notifyCtx , notifyStop := signal . NotifyContext ( ctx , interruptSignals ... )
defer notifyStop ( )
2022-07-27 15:21:21 +00:00
// Clean up idle connections at the end, e.g.
// embedded-postgres can leave an idle connection
// which is caught by goleaks.
defer http . DefaultClient . CloseIdleConnections ( )
2022-05-20 15:51:06 +00:00
var (
tracerProvider * sdktrace . TracerProvider
err error
sqlDriver = "postgres"
)
2022-05-19 22:43:07 +00:00
if trace {
2022-07-27 15:21:21 +00:00
tracerProvider , err = tracing . TracerProvider ( ctx , "coderd" )
2022-05-19 22:43:07 +00:00
if err != nil {
2022-07-27 15:21:21 +00:00
logger . Warn ( ctx , "failed to start telemetry exporter" , slog . Error ( err ) )
2022-05-19 22:43:07 +00:00
} else {
2022-07-27 15:21:21 +00:00
// allow time for traces to flush even if command context is canceled
2022-05-19 22:43:07 +00:00
defer func ( ) {
2022-07-27 15:21:21 +00:00
_ = shutdownWithTimeout ( tracerProvider , 5 * time . Second )
2022-05-19 22:43:07 +00:00
} ( )
2022-05-20 15:51:06 +00:00
d , err := tracing . PostgresDriver ( tracerProvider , "coderd.database" )
if err != nil {
2022-07-27 15:21:21 +00:00
logger . Warn ( ctx , "failed to start postgres tracing driver" , slog . Error ( err ) )
2022-05-20 15:51:06 +00:00
} else {
sqlDriver = d
}
2022-05-19 22:43:07 +00:00
}
2022-03-28 22:11:52 +00:00
}
2022-03-24 15:07:33 +00:00
2022-06-15 21:02:18 +00:00
config := createConfig ( cmd )
2022-06-17 05:26:40 +00:00
builtinPostgres := false
2022-06-15 21:02:18 +00:00
// Only use built-in if PostgreSQL URL isn't specified!
if ! inMemoryDatabase && postgresURL == "" {
var closeFunc func ( ) error
cmd . Printf ( "Using built-in PostgreSQL (%s)\n" , config . PostgresPath ( ) )
2022-07-27 15:21:21 +00:00
postgresURL , closeFunc , err = startBuiltinPostgres ( ctx , config , logger )
2022-06-15 21:02:18 +00:00
if err != nil {
return err
}
2022-06-17 05:26:40 +00:00
builtinPostgres = true
2022-06-15 21:02:18 +00:00
defer func ( ) {
2022-07-27 15:21:21 +00:00
cmd . Printf ( "Stopping built-in PostgreSQL...\n" )
2022-06-15 21:02:18 +00:00
// Gracefully shut PostgreSQL down!
_ = closeFunc ( )
2022-07-27 15:21:21 +00:00
cmd . Printf ( "Stopped built-in PostgreSQL\n" )
2022-06-15 21:02:18 +00:00
} ( )
}
2022-03-22 19:17:50 +00:00
listener , err := net . Listen ( "tcp" , address )
if err != nil {
return xerrors . Errorf ( "listen %q: %w" , address , err )
}
defer listener . Close ( )
2022-03-24 19:21:05 +00:00
if tlsEnable {
2022-03-25 00:20:13 +00:00
listener , err = configureTLS ( listener , tlsMinVersion , tlsClientAuth , tlsCertFile , tlsKeyFile , tlsClientCAFile )
2022-03-24 19:21:05 +00:00
if err != nil {
return xerrors . Errorf ( "configure tls: %w" , err )
}
}
2022-03-22 19:17:50 +00:00
tcpAddr , valid := listener . Addr ( ) . ( * net . TCPAddr )
if ! valid {
return xerrors . New ( "must be listening on tcp" )
}
2022-03-24 15:07:33 +00:00
// If just a port is specified, assume localhost.
2022-03-22 19:17:50 +00:00
if tcpAddr . IP . IsUnspecified ( ) {
tcpAddr . IP = net . IPv4 ( 127 , 0 , 0 , 1 )
}
2022-06-15 21:02:18 +00:00
// If no access URL is specified, fallback to the
// bounds URL.
2022-03-22 19:17:50 +00:00
localURL := & url . URL {
Scheme : "http" ,
Host : tcpAddr . String ( ) ,
}
2022-03-24 19:21:05 +00:00
if tlsEnable {
localURL . Scheme = "https"
}
if accessURL == "" {
accessURL = localURL . String ( )
}
2022-04-14 15:29:40 +00:00
var (
2022-07-27 15:21:21 +00:00
ctxTunnel , closeTunnel = context . WithCancel ( ctx )
devTunnel * devtunnel . Tunnel
devTunnelErr <- chan error
2022-04-14 15:29:40 +00:00
)
defer closeTunnel ( )
2022-03-24 15:07:33 +00:00
// If we're attempting to tunnel in dev-mode, the access URL
// needs to be changed to use the tunnel.
2022-06-15 21:02:18 +00:00
if tunnel {
cmd . Printf ( "Opening tunnel so workspaces can connect to your deployment\n" )
2022-07-27 15:21:21 +00:00
devTunnel , devTunnelErr , err = devtunnel . New ( ctxTunnel , logger . Named ( "devtunnel" ) )
2022-06-15 21:02:18 +00:00
if err != nil {
return xerrors . Errorf ( "create tunnel: %w" , err )
2022-03-24 15:07:33 +00:00
}
2022-06-15 21:02:18 +00:00
accessURL = devTunnel . URL
2022-03-22 19:17:50 +00:00
}
2022-04-14 15:29:40 +00:00
2022-07-31 22:49:25 +00:00
accessURLParsed , err := parseURL ( ctx , accessURL )
if err != nil {
return xerrors . Errorf ( "parse URL: %w" , err )
}
2022-06-10 18:35:51 +00:00
// Warn the user if the access URL appears to be a loopback address.
2022-07-31 22:49:25 +00:00
isLocal , err := isLocalURL ( ctx , accessURLParsed )
2022-06-10 18:35:51 +00:00
if isLocal || err != nil {
2022-06-15 21:02:18 +00:00
reason := "could not be resolved"
2022-06-10 18:35:51 +00:00
if isLocal {
2022-06-15 21:02:18 +00:00
reason = "isn't externally reachable"
2022-06-10 18:35:51 +00:00
}
2022-07-31 22:49:25 +00:00
cmd . Printf ( "%s The access URL %s %s, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL with:\n" , cliui . Styles . Warn . Render ( "Warning:" ) , cliui . Styles . Field . Render ( accessURLParsed . String ( ) ) , reason )
2022-06-15 21:02:18 +00:00
cmd . Println ( cliui . Styles . Code . Render ( strings . Join ( os . Args , " " ) + " --tunnel" ) )
2022-03-22 19:17:50 +00:00
}
2022-03-24 15:07:33 +00:00
2022-07-31 22:49:25 +00:00
cmd . Printf ( "View the Web UI: %s\n" , accessURLParsed . String ( ) )
2022-04-06 00:18:26 +00:00
2022-06-15 21:02:18 +00:00
// Used for zero-trust instance identity with Google Cloud.
2022-07-27 15:21:21 +00:00
googleTokenValidator , err := idtoken . NewValidator ( ctx , option . WithoutAuthentication ( ) )
2022-06-15 21:02:18 +00:00
if err != nil {
return err
}
2022-04-06 00:18:26 +00:00
sshKeygenAlgorithm , err := gitsshkey . ParseAlgorithm ( sshKeygenAlgorithmRaw )
if err != nil {
return xerrors . Errorf ( "parse ssh keygen algorithm %s: %w" , sshKeygenAlgorithmRaw , err )
}
2022-04-18 22:40:25 +00:00
turnServer , err := turnconn . New ( & turn . RelayAddressGeneratorStatic {
RelayAddress : net . ParseIP ( turnRelayAddress ) ,
Address : turnRelayAddress ,
} )
if err != nil {
return xerrors . Errorf ( "create turn server: %w" , err )
}
2022-07-27 15:21:21 +00:00
defer turnServer . Close ( )
2022-04-18 22:40:25 +00:00
2022-04-25 04:52:07 +00:00
iceServers := make ( [ ] webrtc . ICEServer , 0 )
for _ , stunServer := range stunServers {
iceServers = append ( iceServers , webrtc . ICEServer {
URLs : [ ] string { stunServer } ,
} )
}
2022-03-24 15:07:33 +00:00
options := & coderd . Options {
2022-03-24 19:21:05 +00:00
AccessURL : accessURLParsed ,
2022-04-25 04:52:07 +00:00
ICEServers : iceServers ,
2022-03-24 15:07:33 +00:00
Logger : logger . Named ( "coderd" ) ,
2022-03-22 19:17:50 +00:00
Database : databasefake . New ( ) ,
Pubsub : database . NewPubsubInMemory ( ) ,
2022-06-21 16:53:36 +00:00
CacheDir : cacheDir ,
2022-06-15 21:02:18 +00:00
GoogleTokenValidator : googleTokenValidator ,
2022-03-31 17:31:06 +00:00
SecureAuthCookie : secureAuthCookie ,
2022-04-06 00:18:26 +00:00
SSHKeygenAlgorithm : sshKeygenAlgorithm ,
2022-04-18 22:40:25 +00:00
TURNServer : turnServer ,
2022-05-19 22:43:07 +00:00
TracerProvider : tracerProvider ,
2022-06-17 05:26:40 +00:00
Telemetry : telemetry . NewNoop ( ) ,
2022-03-24 15:07:33 +00:00
}
2022-04-23 22:58:57 +00:00
if oauth2GithubClientSecret != "" {
2022-08-09 01:49:51 +00:00
options . GithubOAuth2Config , err = configureGithubOAuth2 ( accessURLParsed , oauth2GithubClientID , oauth2GithubClientSecret , oauth2GithubAllowSignups , oauth2GithubAllowedOrganizations , oauth2GithubAllowedTeams , oauth2GithubEnterpriseBaseURL )
2022-04-23 22:58:57 +00:00
if err != nil {
return xerrors . Errorf ( "configure github oauth2: %w" , err )
}
}
2022-08-01 04:05:35 +00:00
if oidcClientSecret != "" {
if oidcClientID == "" {
return xerrors . Errorf ( "OIDC client ID be set!" )
}
if oidcIssuerURL == "" {
return xerrors . Errorf ( "OIDC issuer URL must be set!" )
}
oidcProvider , err := oidc . NewProvider ( ctx , oidcIssuerURL )
if err != nil {
return xerrors . Errorf ( "configure oidc provider: %w" , err )
}
redirectURL , err := accessURLParsed . Parse ( "/api/v2/users/oidc/callback" )
if err != nil {
return xerrors . Errorf ( "parse oidc oauth callback url: %w" , err )
}
options . OIDCConfig = & coderd . OIDCConfig {
OAuth2Config : & oauth2 . Config {
ClientID : oidcClientID ,
ClientSecret : oidcClientSecret ,
RedirectURL : redirectURL . String ( ) ,
Endpoint : oidcProvider . Endpoint ( ) ,
Scopes : oidcScopes ,
} ,
Verifier : oidcProvider . Verifier ( & oidc . Config {
ClientID : oidcClientID ,
} ) ,
EmailDomain : oidcEmailDomain ,
AllowSignups : oidcAllowSignups ,
}
}
2022-06-15 21:02:18 +00:00
if inMemoryDatabase {
options . Database = databasefake . New ( )
options . Pubsub = database . NewPubsubInMemory ( )
} else {
2022-05-20 15:51:06 +00:00
sqlDB , err := sql . Open ( sqlDriver , postgresURL )
2022-03-24 15:07:33 +00:00
if err != nil {
return xerrors . Errorf ( "dial postgres: %w" , err )
}
2022-07-27 15:21:21 +00:00
defer sqlDB . Close ( )
2022-03-24 15:07:33 +00:00
err = sqlDB . Ping ( )
if err != nil {
return xerrors . Errorf ( "ping postgres: %w" , err )
}
err = database . MigrateUp ( sqlDB )
if err != nil {
return xerrors . Errorf ( "migrate up: %w" , err )
}
options . Database = database . New ( sqlDB )
2022-07-27 15:21:21 +00:00
options . Pubsub , err = database . NewPubsub ( ctx , sqlDB , postgresURL )
2022-03-24 15:07:33 +00:00
if err != nil {
return xerrors . Errorf ( "create pubsub: %w" , err )
}
2022-07-27 15:21:21 +00:00
defer options . Pubsub . Close ( )
2022-03-24 15:07:33 +00:00
}
2022-07-27 15:21:21 +00:00
deploymentID , err := options . Database . GetDeploymentID ( ctx )
2022-06-17 05:26:40 +00:00
if errors . Is ( err , sql . ErrNoRows ) {
err = nil
}
if err != nil {
return xerrors . Errorf ( "get deployment id: %w" , err )
}
if deploymentID == "" {
deploymentID = uuid . NewString ( )
2022-07-27 15:21:21 +00:00
err = options . Database . InsertDeploymentID ( ctx , deploymentID )
2022-06-17 05:26:40 +00:00
if err != nil {
return xerrors . Errorf ( "set deployment id: %w" , err )
}
}
// Parse the raw telemetry URL!
2022-07-31 22:49:25 +00:00
telemetryURL , err := parseURL ( ctx , telemetryURL )
2022-06-17 05:26:40 +00:00
if err != nil {
return xerrors . Errorf ( "parse telemetry url: %w" , err )
}
2022-06-17 14:02:44 +00:00
// Disable telemetry if the in-memory database is used unless explicitly defined!
if inMemoryDatabase && ! cmd . Flags ( ) . Changed ( "telemetry" ) {
telemetryEnable = false
}
if telemetryEnable {
2022-06-17 05:26:40 +00:00
options . Telemetry , err = telemetry . New ( telemetry . Options {
BuiltinPostgres : builtinPostgres ,
DeploymentID : deploymentID ,
Database : options . Database ,
Logger : logger . Named ( "telemetry" ) ,
URL : telemetryURL ,
GitHubOAuth : oauth2GithubClientID != "" ,
2022-08-01 04:05:35 +00:00
OIDCAuth : oidcClientID != "" ,
OIDCIssuerURL : oidcIssuerURL ,
2022-06-17 05:26:40 +00:00
Prometheus : promEnabled ,
STUN : len ( stunServers ) != 0 ,
Tunnel : tunnel ,
} )
if err != nil {
return xerrors . Errorf ( "create telemetry reporter: %w" , err )
}
defer options . Telemetry . Close ( )
}
2022-08-08 15:09:46 +00:00
// This prevents the pprof import from being accidentally deleted.
_ = pprof . Handler
if pprofEnabled {
//nolint:revive
defer serveHandler ( ctx , logger , nil , pprofAddress , "pprof" ) ( )
}
if promEnabled {
options . PrometheusRegistry = prometheus . NewRegistry ( )
2022-08-09 01:08:42 +00:00
closeUsersFunc , err := prometheusmetrics . ActiveUsers ( ctx , options . PrometheusRegistry , options . Database , 0 )
2022-08-08 15:09:46 +00:00
if err != nil {
return xerrors . Errorf ( "register active users prometheus metric: %w" , err )
}
2022-08-09 01:08:42 +00:00
defer closeUsersFunc ( )
closeWorkspacesFunc , err := prometheusmetrics . Workspaces ( ctx , options . PrometheusRegistry , options . Database , 0 )
if err != nil {
return xerrors . Errorf ( "register workspaces prometheus metric: %w" , err )
}
defer closeWorkspacesFunc ( )
2022-08-08 15:09:46 +00:00
//nolint:revive
defer serveHandler ( ctx , logger , promhttp . InstrumentMetricHandler (
options . PrometheusRegistry , promhttp . HandlerFor ( options . PrometheusRegistry , promhttp . HandlerOpts { } ) ,
) , promAddress , "prometheus" ) ( )
}
2022-05-26 03:14:08 +00:00
coderAPI := coderd . New ( options )
2022-07-27 15:21:21 +00:00
defer coderAPI . Close ( )
2022-03-22 19:17:50 +00:00
client := codersdk . New ( localURL )
2022-03-24 19:21:05 +00:00
if tlsEnable {
2022-03-25 00:20:13 +00:00
// Secure transport isn't needed for locally communicating!
2022-03-24 19:21:05 +00:00
client . HTTPClient . Transport = & http . Transport {
2022-03-25 00:20:13 +00:00
TLSClientConfig : & tls . Config {
//nolint:gosec
InsecureSkipVerify : true ,
} ,
2022-03-24 19:21:05 +00:00
}
}
2022-03-22 19:17:50 +00:00
2022-07-27 15:21:21 +00:00
// Since errCh only has one buffered slot, all routines
// sending on it must be wrapped in a select/default to
// avoid leaving dangling goroutines waiting for the
// channel to be consumed.
2022-04-18 20:11:59 +00:00
errCh := make ( chan error , 1 )
2022-03-24 15:07:33 +00:00
provisionerDaemons := make ( [ ] * provisionerd . Server , 0 )
2022-07-27 15:21:21 +00:00
defer func ( ) {
// We have no graceful shutdown of provisionerDaemons
// here because that's handled at the end of main, this
// is here in case the program exits early.
for _ , daemon := range provisionerDaemons {
_ = daemon . Close ( )
}
} ( )
2022-03-28 19:26:41 +00:00
for i := 0 ; uint8 ( i ) < provisionerDaemonCount ; i ++ {
2022-07-27 15:21:21 +00:00
daemon , err := newProvisionerDaemon ( ctx , coderAPI , logger , cacheDir , errCh , false )
2022-03-24 15:07:33 +00:00
if err != nil {
return xerrors . Errorf ( "create provisioner daemon: %w" , err )
}
2022-07-27 15:21:21 +00:00
provisionerDaemons = append ( provisionerDaemons , daemon )
}
shutdownConnsCtx , shutdownConns := context . WithCancel ( ctx )
defer shutdownConns ( )
server := & http . Server {
// These errors are typically noise like "TLS: EOF". Vault does similar:
// https://github.com/hashicorp/vault/blob/e2490059d0711635e529a4efcbaa1b26998d6e1c/command/server.go#L2714
2022-08-21 22:32:53 +00:00
ErrorLog : log . New ( io . Discard , "" , 0 ) ,
Handler : coderAPI . Handler ,
ReadHeaderTimeout : time . Minute ,
2022-07-27 15:21:21 +00:00
BaseContext : func ( _ net . Listener ) context . Context {
return shutdownConnsCtx
} ,
2022-03-22 19:17:50 +00:00
}
2022-03-24 15:07:33 +00:00
defer func ( ) {
2022-07-27 15:21:21 +00:00
_ = shutdownWithTimeout ( server , 5 * time . Second )
2022-03-24 15:07:33 +00:00
} ( )
2022-03-22 19:17:50 +00:00
2022-07-27 15:21:21 +00:00
eg := errgroup . Group { }
eg . Go ( func ( ) error {
// Make sure to close the tunnel listener if we exit so the
// errgroup doesn't wait forever!
if tunnel {
defer devTunnel . Listener . Close ( )
2022-03-24 19:21:05 +00:00
}
2022-06-10 18:38:11 +00:00
2022-07-27 15:21:21 +00:00
return server . Serve ( listener )
} )
if tunnel {
eg . Go ( func ( ) error {
defer listener . Close ( )
2022-06-10 18:38:11 +00:00
2022-07-27 15:21:21 +00:00
return server . Serve ( devTunnel . Listener )
2022-06-10 18:38:11 +00:00
} )
2022-07-27 15:21:21 +00:00
}
go func ( ) {
select {
case errCh <- eg . Wait ( ) :
default :
2022-06-10 18:38:11 +00:00
}
2022-03-22 19:17:50 +00:00
} ( )
2022-07-27 15:21:21 +00:00
hasFirstUser , err := client . HasFirstUser ( ctx )
2022-06-15 21:02:18 +00:00
if ! hasFirstUser && err == nil {
cmd . Println ( )
cmd . Println ( "Get started by creating the first user (in a new terminal):" )
2022-07-31 22:49:25 +00:00
cmd . Println ( cliui . Styles . Code . Render ( "coder login " + accessURLParsed . String ( ) ) )
2022-03-22 19:17:50 +00:00
}
2022-06-15 21:02:18 +00:00
cmd . Println ( "\n==> Logs will stream in below (press ctrl+c to gracefully exit):" )
2022-03-24 15:07:33 +00:00
// Updates the systemd status from activating to activated.
_ , err = daemon . SdNotify ( false , daemon . SdNotifyReady )
if err != nil {
return xerrors . Errorf ( "notify systemd: %w" , err )
}
2022-05-20 10:57:02 +00:00
autobuildPoller := time . NewTicker ( autobuildPollInterval )
defer autobuildPoller . Stop ( )
2022-07-27 15:21:21 +00:00
autobuildExecutor := executor . New ( ctx , options . Database , logger , autobuildPoller . C )
2022-05-20 10:57:02 +00:00
autobuildExecutor . Run ( )
2022-05-11 22:03:02 +00:00
2022-07-27 15:21:21 +00:00
// This is helpful for tests, but can be silently ignored.
// Coder may be ran as users that don't have permission to write in the homedir,
// such as via the systemd service.
_ = config . URL ( ) . Write ( client . URL . String ( ) )
// Currently there is no way to ask the server to shut
// itself down, so any exit signal will result in a non-zero
// exit of the server.
var exitErr error
2022-03-22 19:17:50 +00:00
select {
2022-08-18 13:25:32 +00:00
case <- notifyCtx . Done ( ) :
exitErr = notifyCtx . Err ( )
2022-07-27 15:21:21 +00:00
_ , _ = fmt . Fprintln ( cmd . OutOrStdout ( ) , cliui . Styles . Bold . Render (
"Interrupt caught, gracefully exiting. Use ctrl+\\ to force quit" ,
) )
case exitErr = <- devTunnelErr :
if exitErr == nil {
exitErr = xerrors . New ( "dev tunnel closed unexpectedly" )
2022-04-14 15:29:40 +00:00
}
2022-07-27 15:21:21 +00:00
case exitErr = <- errCh :
2022-03-24 15:07:33 +00:00
}
2022-07-27 15:21:21 +00:00
if exitErr != nil && ! xerrors . Is ( exitErr , context . Canceled ) {
cmd . Printf ( "Unexpected error, shutting down server: %s\n" , exitErr )
}
// Begin clean shut down stage, we try to shut down services
// gracefully in an order that gives the best experience.
// This procedure should not differ greatly from the order
// of `defer`s in this function, but allows us to inform
// the user about what's going on and handle errors more
// explicitly.
2022-03-24 15:07:33 +00:00
_ , err = daemon . SdNotify ( false , daemon . SdNotifyStopping )
if err != nil {
2022-07-27 15:21:21 +00:00
cmd . Printf ( "Notify systemd failed: %s" , err )
2022-03-24 15:07:33 +00:00
}
2022-07-27 15:21:21 +00:00
// Stop accepting new connections without interrupting
// in-flight requests, give in-flight requests 5 seconds to
// complete.
cmd . Println ( "Shutting down API server..." )
err = shutdownWithTimeout ( server , 5 * time . Second )
if err != nil {
cmd . Printf ( "API server shutdown took longer than 5s: %s" , err )
} else {
cmd . Printf ( "Gracefully shut down API server\n" )
2022-03-22 19:17:50 +00:00
}
2022-07-27 15:21:21 +00:00
// Cancel any remaining in-flight requests.
shutdownConns ( )
2022-03-24 15:07:33 +00:00
2022-07-27 15:21:21 +00:00
// Shut down provisioners before waiting for WebSockets
// connections to close.
var wg sync . WaitGroup
for i , provisionerDaemon := range provisionerDaemons {
id := i + 1
provisionerDaemon := provisionerDaemon
wg . Add ( 1 )
go func ( ) {
defer wg . Done ( )
if verbose {
cmd . Printf ( "Shutting down provisioner daemon %d...\n" , id )
}
err := shutdownWithTimeout ( provisionerDaemon , 5 * time . Second )
if err != nil {
cmd . PrintErrf ( "Failed to shutdown provisioner daemon %d: %s\n" , id , err )
return
}
err = provisionerDaemon . Close ( )
if err != nil {
cmd . PrintErrf ( "Close provisioner daemon %d: %s\n" , id , err )
return
}
if verbose {
cmd . Printf ( "Gracefully shut down provisioner daemon %d\n" , id )
}
} ( )
}
wg . Wait ( )
cmd . Println ( "Waiting for WebSocket connections to close..." )
_ = coderAPI . Close ( )
cmd . Println ( "Done wainting for WebSocket connections" )
// Close tunnel after we no longer have in-flight connections.
2022-06-15 21:02:18 +00:00
if tunnel {
cmd . Println ( "Waiting for tunnel to close..." )
2022-04-14 15:29:40 +00:00
closeTunnel ( )
2022-07-27 15:21:21 +00:00
<- devTunnelErr
cmd . Println ( "Done waiting for tunnel" )
2022-04-14 15:29:40 +00:00
}
2022-06-17 05:26:40 +00:00
// Ensures a last report can be sent before exit!
options . Telemetry . Close ( )
2022-07-27 15:21:21 +00:00
// Trigger context cancellation for any remaining services.
cancel ( )
return exitErr
2022-03-22 19:17:50 +00:00
} ,
}
2022-03-28 19:26:41 +00:00
2022-06-15 21:02:18 +00:00
root . AddCommand ( & cobra . Command {
Use : "postgres-builtin-url" ,
Short : "Output the connection URL for the built-in PostgreSQL deployment." ,
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
cfg := createConfig ( cmd )
url , err := embeddedPostgresURL ( cfg )
if err != nil {
return err
}
cmd . Println ( cliui . Styles . Code . Render ( "psql \"" + url + "\"" ) )
return nil
} ,
} )
2022-07-14 21:51:44 +00:00
root . AddCommand ( & cobra . Command {
Use : "postgres-builtin-serve" ,
Short : "Run the built-in PostgreSQL deployment." ,
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
cfg := createConfig ( cmd )
2022-07-26 14:23:32 +00:00
logger := slog . Make ( sloghuman . Sink ( cmd . ErrOrStderr ( ) ) )
2022-07-14 21:51:44 +00:00
if verbose {
logger = logger . Leveled ( slog . LevelDebug )
}
url , closePg , err := startBuiltinPostgres ( cmd . Context ( ) , cfg , logger )
if err != nil {
return err
}
defer func ( ) { _ = closePg ( ) } ( )
cmd . Println ( cliui . Styles . Code . Render ( "psql \"" + url + "\"" ) )
stopChan := make ( chan os . Signal , 1 )
defer signal . Stop ( stopChan )
signal . Notify ( stopChan , os . Interrupt )
<- stopChan
return nil
} ,
} )
2022-05-20 10:57:02 +00:00
cliflag . DurationVarP ( root . Flags ( ) , & autobuildPollInterval , "autobuild-poll-interval" , "" , "CODER_AUTOBUILD_POLL_INTERVAL" , time . Minute , "Specifies the interval at which to poll for and execute automated workspace build operations." )
2022-05-03 12:48:02 +00:00
cliflag . StringVarP ( root . Flags ( ) , & accessURL , "access-url" , "" , "CODER_ACCESS_URL" , "" , "Specifies the external URL to access Coder." )
cliflag . StringVarP ( root . Flags ( ) , & address , "address" , "a" , "CODER_ADDRESS" , "127.0.0.1:3000" , "The address to serve the API and dashboard." )
cliflag . BoolVarP ( root . Flags ( ) , & promEnabled , "prometheus-enable" , "" , "CODER_PROMETHEUS_ENABLE" , false , "Enable serving prometheus metrics on the addressdefined by --prometheus-address." )
cliflag . StringVarP ( root . Flags ( ) , & promAddress , "prometheus-address" , "" , "CODER_PROMETHEUS_ADDRESS" , "127.0.0.1:2112" , "The address to serve prometheus metrics." )
2022-05-03 19:54:10 +00:00
cliflag . BoolVarP ( root . Flags ( ) , & pprofEnabled , "pprof-enable" , "" , "CODER_PPROF_ENABLE" , false , "Enable serving pprof metrics on the address defined by --pprof-address." )
2022-05-03 12:48:02 +00:00
cliflag . StringVarP ( root . Flags ( ) , & pprofAddress , "pprof-address" , "" , "CODER_PPROF_ADDRESS" , "127.0.0.1:6060" , "The address to serve pprof." )
2022-06-10 14:00:00 +00:00
defaultCacheDir := filepath . Join ( os . TempDir ( ) , "coder-cache" )
if dir := os . Getenv ( "CACHE_DIRECTORY" ) ; dir != "" {
// For compatibility with systemd.
defaultCacheDir = dir
}
cliflag . StringVarP ( root . Flags ( ) , & cacheDir , "cache-dir" , "" , "CODER_CACHE_DIRECTORY" , defaultCacheDir , "Specifies a directory to cache binaries for provision operations. If unspecified and $CACHE_DIRECTORY is set, it will be used for compatibility with systemd." )
2022-06-15 21:02:18 +00:00
cliflag . BoolVarP ( root . Flags ( ) , & inMemoryDatabase , "in-memory" , "" , "CODER_INMEMORY" , false ,
"Specifies whether data will be stored in an in-memory database." )
_ = root . Flags ( ) . MarkHidden ( "in-memory" )
cliflag . StringVarP ( root . Flags ( ) , & postgresURL , "postgres-url" , "" , "CODER_PG_CONNECTION_URL" , "" , "The URL of a PostgreSQL database to connect to. If empty, PostgreSQL binaries will be downloaded from Maven (https://repo1.maven.org/maven2) and store all data in the config root. Access the built-in database with \"coder server postgres-builtin-url\"" )
2022-04-26 12:19:17 +00:00
cliflag . Uint8VarP ( root . Flags ( ) , & provisionerDaemonCount , "provisioner-daemons" , "" , "CODER_PROVISIONER_DAEMONS" , 3 , "The amount of provisioner daemons to create on start." )
2022-04-23 22:58:57 +00:00
cliflag . StringVarP ( root . Flags ( ) , & oauth2GithubClientID , "oauth2-github-client-id" , "" , "CODER_OAUTH2_GITHUB_CLIENT_ID" , "" ,
"Specifies a client ID to use for oauth2 with GitHub." )
cliflag . StringVarP ( root . Flags ( ) , & oauth2GithubClientSecret , "oauth2-github-client-secret" , "" , "CODER_OAUTH2_GITHUB_CLIENT_SECRET" , "" ,
"Specifies a client secret to use for oauth2 with GitHub." )
cliflag . StringArrayVarP ( root . Flags ( ) , & oauth2GithubAllowedOrganizations , "oauth2-github-allowed-orgs" , "" , "CODER_OAUTH2_GITHUB_ALLOWED_ORGS" , nil ,
"Specifies organizations the user must be a member of to authenticate with GitHub." )
2022-07-09 02:37:18 +00:00
cliflag . StringArrayVarP ( root . Flags ( ) , & oauth2GithubAllowedTeams , "oauth2-github-allowed-teams" , "" , "CODER_OAUTH2_GITHUB_ALLOWED_TEAMS" , nil ,
"Specifies teams inside organizations the user must be a member of to authenticate with GitHub. Formatted as: <organization-name>/<team-slug>." )
2022-04-23 22:58:57 +00:00
cliflag . BoolVarP ( root . Flags ( ) , & oauth2GithubAllowSignups , "oauth2-github-allow-signups" , "" , "CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS" , false ,
"Specifies whether new users can sign up with GitHub." )
2022-08-09 01:49:51 +00:00
cliflag . StringVarP ( root . Flags ( ) , & oauth2GithubEnterpriseBaseURL , "oauth2-github-enterprise-base-url" , "" , "CODER_OAUTH2_GITHUB_ENTERPRISE_BASE_URL" , "" ,
"Specifies the base URL of a GitHub Enterprise instance to use for oauth2." )
2022-08-01 04:05:35 +00:00
cliflag . BoolVarP ( root . Flags ( ) , & oidcAllowSignups , "oidc-allow-signups" , "" , "CODER_OIDC_ALLOW_SIGNUPS" , true ,
"Specifies whether new users can sign up with OIDC." )
cliflag . StringVarP ( root . Flags ( ) , & oidcClientID , "oidc-client-id" , "" , "CODER_OIDC_CLIENT_ID" , "" ,
"Specifies a client ID to use for OIDC." )
cliflag . StringVarP ( root . Flags ( ) , & oidcClientSecret , "oidc-client-secret" , "" , "CODER_OIDC_CLIENT_SECRET" , "" ,
"Specifies a client secret to use for OIDC." )
cliflag . StringVarP ( root . Flags ( ) , & oidcEmailDomain , "oidc-email-domain" , "" , "CODER_OIDC_EMAIL_DOMAIN" , "" ,
"Specifies an email domain that clients authenticating with OIDC must match." )
cliflag . StringVarP ( root . Flags ( ) , & oidcIssuerURL , "oidc-issuer-url" , "" , "CODER_OIDC_ISSUER_URL" , "" ,
"Specifies an issuer URL to use for OIDC." )
cliflag . StringArrayVarP ( root . Flags ( ) , & oidcScopes , "oidc-scopes" , "" , "CODER_OIDC_SCOPES" , [ ] string { oidc . ScopeOpenID , "profile" , "email" } ,
"Specifies scopes to grant when authenticating with OIDC." )
2022-07-26 13:27:48 +00:00
enableTelemetryByDefault := ! isTest ( )
cliflag . BoolVarP ( root . Flags ( ) , & telemetryEnable , "telemetry" , "" , "CODER_TELEMETRY" , enableTelemetryByDefault , "Specifies whether telemetry is enabled or not. Coder collects anonymized usage data to help improve our product." )
2022-06-17 05:26:40 +00:00
cliflag . StringVarP ( root . Flags ( ) , & telemetryURL , "telemetry-url" , "" , "CODER_TELEMETRY_URL" , "https://telemetry.coder.com" , "Specifies a URL to send telemetry to." )
_ = root . Flags ( ) . MarkHidden ( "telemetry-url" )
2022-03-28 19:26:41 +00:00
cliflag . BoolVarP ( root . Flags ( ) , & tlsEnable , "tls-enable" , "" , "CODER_TLS_ENABLE" , false , "Specifies if TLS will be enabled" )
cliflag . StringVarP ( root . Flags ( ) , & tlsCertFile , "tls-cert-file" , "" , "CODER_TLS_CERT_FILE" , "" ,
2022-03-24 19:21:05 +00:00
"Specifies the path to the certificate for TLS. It requires a PEM-encoded file. " +
"To configure the listener to use a CA certificate, concatenate the primary certificate " +
2022-03-28 19:26:41 +00:00
"and the CA certificate together. The primary certificate should appear first in the combined file" )
cliflag . StringVarP ( root . Flags ( ) , & tlsClientCAFile , "tls-client-ca-file" , "" , "CODER_TLS_CLIENT_CA_FILE" , "" ,
"PEM-encoded Certificate Authority file used for checking the authenticity of client" )
cliflag . StringVarP ( root . Flags ( ) , & tlsClientAuth , "tls-client-auth" , "" , "CODER_TLS_CLIENT_AUTH" , "request" ,
2022-03-24 19:21:05 +00:00
` Specifies the policy the server will follow for TLS Client Authentication. ` +
2022-03-28 19:26:41 +00:00
` Accepted values are "none", "request", "require-any", "verify-if-given", or "require-and-verify" ` )
cliflag . StringVarP ( root . Flags ( ) , & tlsKeyFile , "tls-key-file" , "" , "CODER_TLS_KEY_FILE" , "" ,
"Specifies the path to the private key for the certificate. It requires a PEM-encoded file" )
cliflag . StringVarP ( root . Flags ( ) , & tlsMinVersion , "tls-min-version" , "" , "CODER_TLS_MIN_VERSION" , "tls12" ,
` Specifies the minimum supported version of TLS. Accepted values are "tls10", "tls11", "tls12" or "tls13" ` )
2022-06-15 21:02:18 +00:00
cliflag . BoolVarP ( root . Flags ( ) , & tunnel , "tunnel" , "" , "CODER_TUNNEL" , false ,
"Workspaces must be able to reach the `access-url`. This overrides your access URL with a public access URL that tunnels your Coder deployment." )
2022-04-25 04:52:07 +00:00
cliflag . StringArrayVarP ( root . Flags ( ) , & stunServers , "stun-server" , "" , "CODER_STUN_SERVERS" , [ ] string {
"stun:stun.l.google.com:19302" ,
} , "Specify URLs for STUN servers to enable P2P connections." )
2022-05-19 22:43:07 +00:00
cliflag . BoolVarP ( root . Flags ( ) , & trace , "trace" , "" , "CODER_TRACE" , false , "Specifies if application tracing data is collected" )
2022-04-18 22:40:25 +00:00
cliflag . StringVarP ( root . Flags ( ) , & turnRelayAddress , "turn-relay-address" , "" , "CODER_TURN_RELAY_ADDRESS" , "127.0.0.1" ,
"Specifies the address to bind TURN connections." )
2022-03-31 17:31:06 +00:00
cliflag . BoolVarP ( root . Flags ( ) , & secureAuthCookie , "secure-auth-cookie" , "" , "CODER_SECURE_AUTH_COOKIE" , false , "Specifies if the 'Secure' property is set on browser session cookies" )
2022-04-06 00:18:26 +00:00
cliflag . StringVarP ( root . Flags ( ) , & sshKeygenAlgorithmRaw , "ssh-keygen-algorithm" , "" , "CODER_SSH_KEYGEN_ALGORITHM" , "ed25519" , "Specifies the algorithm to use for generating ssh keys. " +
` Accepted values are "ed25519", "ecdsa", or "rsa4096" ` )
2022-04-19 16:40:01 +00:00
cliflag . BoolVarP ( root . Flags ( ) , & spooky , "spooky" , "" , "" , false , "Specifies spookiness level" )
2022-05-03 21:13:30 +00:00
cliflag . BoolVarP ( root . Flags ( ) , & verbose , "verbose" , "v" , "CODER_VERBOSE" , false , "Enables verbose logging." )
2022-04-19 16:40:01 +00:00
_ = root . Flags ( ) . MarkHidden ( "spooky" )
2022-03-22 19:17:50 +00:00
return root
}
2022-07-31 22:49:25 +00:00
// parseURL parses a string into a URL. It works around some technically correct
// but undesired behavior of url.Parse by prepending a scheme if one does not
// exist so that the URL does not get parsed improprely.
func parseURL ( ctx context . Context , u string ) ( * url . URL , error ) {
var (
hasScheme = strings . HasPrefix ( u , "http:" ) || strings . HasPrefix ( u , "https:" )
)
if ! hasScheme {
// Append a scheme if it doesn't have one. Otherwise the hostname
// will likely get parsed as the scheme and cause methods like Hostname()
// to return an empty string, largely obviating the purpose of this
// function.
u = "https://" + u
}
parsed , err := url . Parse ( u )
if err != nil {
return nil , err
}
// If the specified url is a loopback device and no scheme has been
// specified, prefer http over https. It's unlikely anyone intends to use
// https on a loopback and if they do they can specify a scheme.
if local , _ := isLocalURL ( ctx , parsed ) ; local && ! hasScheme {
parsed . Scheme = "http"
}
return parsed , nil
}
// isLocalURL returns true if the hostname of the provided URL appears to
// resolve to a loopback address.
func isLocalURL ( ctx context . Context , u * url . URL ) ( bool , error ) {
resolver := & net . Resolver { }
ips , err := resolver . LookupIPAddr ( ctx , u . Hostname ( ) )
if err != nil {
return false , err
}
for _ , ip := range ips {
if ip . IP . IsLoopback ( ) {
return true , nil
}
}
return false , nil
}
2022-07-27 15:21:21 +00:00
func shutdownWithTimeout ( s interface { Shutdown ( context . Context ) error } , timeout time . Duration ) error {
ctx , cancel := context . WithTimeout ( context . Background ( ) , timeout )
defer cancel ( )
return s . Shutdown ( ctx )
}
2022-05-19 22:47:45 +00:00
// nolint:revive
2022-05-26 03:14:08 +00:00
func newProvisionerDaemon ( ctx context . Context , coderAPI * coderd . API ,
2022-07-27 15:21:21 +00:00
logger slog . Logger , cacheDir string , errCh chan error , dev bool ,
) ( srv * provisionerd . Server , err error ) {
ctx , cancel := context . WithCancel ( ctx )
defer func ( ) {
if err != nil {
cancel ( )
}
} ( )
err = os . MkdirAll ( cacheDir , 0 o700 )
2022-03-30 22:59:54 +00:00
if err != nil {
return nil , xerrors . Errorf ( "mkdir %q: %w" , cacheDir , err )
}
2022-03-22 19:17:50 +00:00
terraformClient , terraformServer := provisionersdk . TransportPipe ( )
go func ( ) {
2022-07-27 15:21:21 +00:00
<- ctx . Done ( )
_ = terraformClient . Close ( )
_ = terraformServer . Close ( )
} ( )
go func ( ) {
defer cancel ( )
2022-03-22 19:17:50 +00:00
err := terraform . Serve ( ctx , & terraform . ServeOptions {
ServeOptions : & provisionersdk . ServeOptions {
Listener : terraformServer ,
} ,
2022-03-28 19:57:19 +00:00
CachePath : cacheDir ,
Logger : logger ,
2022-03-22 19:17:50 +00:00
} )
2022-06-22 23:11:52 +00:00
if err != nil && ! xerrors . Is ( err , context . Canceled ) {
2022-07-27 15:21:21 +00:00
select {
case errCh <- err :
default :
}
2022-03-22 19:17:50 +00:00
}
} ( )
2022-03-29 19:59:32 +00:00
tempDir , err := os . MkdirTemp ( "" , "provisionerd" )
2022-03-22 19:17:50 +00:00
if err != nil {
return nil , err
}
2022-03-29 19:59:32 +00:00
2022-05-19 22:47:45 +00:00
provisioners := provisionerd . Provisioners {
string ( database . ProvisionerTypeTerraform ) : proto . NewDRPCProvisionerClient ( provisionersdk . Conn ( terraformClient ) ) ,
}
// include echo provisioner when in dev mode
if dev {
echoClient , echoServer := provisionersdk . TransportPipe ( )
go func ( ) {
2022-07-27 15:21:21 +00:00
<- ctx . Done ( )
_ = echoClient . Close ( )
_ = echoServer . Close ( )
} ( )
go func ( ) {
defer cancel ( )
2022-06-16 15:09:22 +00:00
err := echo . Serve ( ctx , afero . NewOsFs ( ) , & provisionersdk . ServeOptions { Listener : echoServer } )
2022-05-19 22:47:45 +00:00
if err != nil {
2022-07-27 15:21:21 +00:00
select {
case errCh <- err :
default :
}
2022-05-19 22:47:45 +00:00
}
} ( )
provisioners [ string ( database . ProvisionerTypeEcho ) ] = proto . NewDRPCProvisionerClient ( provisionersdk . Conn ( echoClient ) )
}
2022-05-26 03:14:08 +00:00
return provisionerd . New ( coderAPI . ListenProvisionerDaemon , & provisionerd . Options {
2022-03-22 19:17:50 +00:00
Logger : logger ,
2022-04-11 23:54:30 +00:00
PollInterval : 500 * time . Millisecond ,
UpdateInterval : 500 * time . Millisecond ,
2022-05-19 22:47:45 +00:00
Provisioners : provisioners ,
WorkDirectory : tempDir ,
2022-03-22 19:17:50 +00:00
} ) , nil
}
2022-03-24 19:21:05 +00:00
2022-04-19 16:40:01 +00:00
// nolint: revive
func printLogo ( cmd * cobra . Command , spooky bool ) {
if spooky {
2022-06-15 21:02:18 +00:00
_ , _ = fmt . Fprintf ( cmd . OutOrStdout ( ) , ` ▄ █ █ █ █ ▄ ▒ █ █ █ █ █ ▓ █ █ █ █ █ ▄ ▓ █ █ █ █ █ █ █ ▀ █ █ █
▒ █ █ ▀ ▀ █ ▒ █ █ ▒ █ █ ▒ ▒ █ █ ▀ █ █ ▌ ▓ █ ▀ ▓ █ █ ▒ █ █ ▒
▒ ▓ █ ▄ ▒ █ █ ░ █ █ ▒ ░ █ █ █ ▌ ▒ █ █ █ ▓ █ █ ░ ▄ █ ▒
▒ ▓ ▓ ▄ ▄ █ █ ▒ ▒ █ █ █ █ ░ ░ ▓ █ ▄ ▌ ▒ ▓ █ ▄ ▒ █ █ ▀ ▀ █ ▄
▒ ▓ █ █ █ ▀ ░ ░ █ █ █ █ ▓ ▒ ░ ░ ▒ █ █ █ █ ▓ ░ ▒ █ █ █ █ ▒ ░ █ █ ▓ ▒ █ █ ▒
░ ░ ▒ ▒ ░ ░ ▒ ░ ▒ ░ ▒ ░ ▒ ▒ ▓ ▒ ░ ░ ▒ ░ ░ ░ ▒ ▓ ░ ▒ ▓ ░
░ ▒ ░ ▒ ▒ ░ ░ ▒ ▒ ░ ░ ░ ░ ▒ ░ ▒ ░
░ ░ ░ ░ ▒ ░ ░ ░ ░ ░ ░ ░
░ ░ ░ ░ ░ ░ ░ ░
░ ░
2022-04-19 16:40:01 +00:00
` )
return
}
2022-06-15 21:02:18 +00:00
_ , _ = fmt . Fprintf ( cmd . OutOrStdout ( ) , "%s - Remote development on your infrastucture\n" , cliui . Styles . Bold . Render ( "Coder " + buildinfo . Version ( ) ) )
2022-03-24 19:21:05 +00:00
}
2022-03-25 00:20:13 +00:00
func configureTLS ( listener net . Listener , tlsMinVersion , tlsClientAuth , tlsCertFile , tlsKeyFile , tlsClientCAFile string ) ( net . Listener , error ) {
tlsConfig := & tls . Config {
MinVersion : tls . VersionTLS12 ,
}
2022-03-24 19:21:05 +00:00
switch tlsMinVersion {
case "tls10" :
tlsConfig . MinVersion = tls . VersionTLS10
case "tls11" :
tlsConfig . MinVersion = tls . VersionTLS11
case "tls12" :
tlsConfig . MinVersion = tls . VersionTLS12
case "tls13" :
tlsConfig . MinVersion = tls . VersionTLS13
default :
return nil , xerrors . Errorf ( "unrecognized tls version: %q" , tlsMinVersion )
}
switch tlsClientAuth {
case "none" :
tlsConfig . ClientAuth = tls . NoClientCert
case "request" :
tlsConfig . ClientAuth = tls . RequestClientCert
case "require-any" :
tlsConfig . ClientAuth = tls . RequireAnyClientCert
case "verify-if-given" :
tlsConfig . ClientAuth = tls . VerifyClientCertIfGiven
case "require-and-verify" :
tlsConfig . ClientAuth = tls . RequireAndVerifyClientCert
default :
return nil , xerrors . Errorf ( "unrecognized tls client auth: %q" , tlsClientAuth )
}
if tlsCertFile == "" {
return nil , xerrors . New ( "tls-cert-file is required when tls is enabled" )
}
if tlsKeyFile == "" {
return nil , xerrors . New ( "tls-key-file is required when tls is enabled" )
}
certPEMBlock , err := os . ReadFile ( tlsCertFile )
if err != nil {
return nil , xerrors . Errorf ( "read file %q: %w" , tlsCertFile , err )
}
keyPEMBlock , err := os . ReadFile ( tlsKeyFile )
if err != nil {
return nil , xerrors . Errorf ( "read file %q: %w" , tlsKeyFile , err )
}
keyBlock , _ := pem . Decode ( keyPEMBlock )
if keyBlock == nil {
return nil , xerrors . New ( "decoded pem is blank" )
}
cert , err := tls . X509KeyPair ( certPEMBlock , keyPEMBlock )
if err != nil {
return nil , xerrors . Errorf ( "create key pair: %w" , err )
}
tlsConfig . GetCertificate = func ( chi * tls . ClientHelloInfo ) ( * tls . Certificate , error ) {
return & cert , nil
}
certPool := x509 . NewCertPool ( )
certPool . AppendCertsFromPEM ( certPEMBlock )
tlsConfig . RootCAs = certPool
if tlsClientCAFile != "" {
caPool := x509 . NewCertPool ( )
2022-03-29 19:59:32 +00:00
data , err := os . ReadFile ( tlsClientCAFile )
2022-03-24 19:21:05 +00:00
if err != nil {
return nil , xerrors . Errorf ( "read %q: %w" , tlsClientCAFile , err )
}
if ! caPool . AppendCertsFromPEM ( data ) {
return nil , xerrors . Errorf ( "failed to parse CA certificate in tls-client-ca-file" )
}
tlsConfig . ClientCAs = caPool
}
return tls . NewListener ( listener , tlsConfig ) , nil
}
2022-04-18 17:37:01 +00:00
2022-08-09 01:49:51 +00:00
func configureGithubOAuth2 ( accessURL * url . URL , clientID , clientSecret string , allowSignups bool , allowOrgs [ ] string , rawTeams [ ] string , enterpriseBaseURL string ) ( * coderd . GithubOAuth2Config , error ) {
2022-04-23 22:58:57 +00:00
redirectURL , err := accessURL . Parse ( "/api/v2/users/oauth2/github/callback" )
if err != nil {
return nil , xerrors . Errorf ( "parse github oauth callback url: %w" , err )
}
2022-07-09 02:37:18 +00:00
allowTeams := make ( [ ] coderd . GithubOAuth2Team , 0 , len ( rawTeams ) )
for _ , rawTeam := range rawTeams {
parts := strings . SplitN ( rawTeam , "/" , 2 )
if len ( parts ) != 2 {
return nil , xerrors . Errorf ( "github team allowlist is formatted incorrectly. got %s; wanted <organization>/<team>" , rawTeam )
}
allowTeams = append ( allowTeams , coderd . GithubOAuth2Team {
Organization : parts [ 0 ] ,
Slug : parts [ 1 ] ,
} )
}
2022-08-09 01:49:51 +00:00
createClient := func ( client * http . Client ) ( * github . Client , error ) {
if enterpriseBaseURL != "" {
return github . NewEnterpriseClient ( enterpriseBaseURL , "" , client )
}
return github . NewClient ( client ) , nil
}
endpoint := xgithub . Endpoint
if enterpriseBaseURL != "" {
enterpriseURL , err := url . Parse ( enterpriseBaseURL )
if err != nil {
return nil , xerrors . Errorf ( "parse enterprise base url: %w" , err )
}
authURL , err := enterpriseURL . Parse ( "/login/oauth/authorize" )
if err != nil {
return nil , xerrors . Errorf ( "parse enterprise auth url: %w" , err )
}
tokenURL , err := enterpriseURL . Parse ( "/login/oauth/access_token" )
if err != nil {
return nil , xerrors . Errorf ( "parse enterprise token url: %w" , err )
}
endpoint = oauth2 . Endpoint {
AuthURL : authURL . String ( ) ,
TokenURL : tokenURL . String ( ) ,
}
}
2022-04-23 22:58:57 +00:00
return & coderd . GithubOAuth2Config {
OAuth2Config : & oauth2 . Config {
ClientID : clientID ,
ClientSecret : clientSecret ,
2022-08-09 01:49:51 +00:00
Endpoint : endpoint ,
2022-04-23 22:58:57 +00:00
RedirectURL : redirectURL . String ( ) ,
Scopes : [ ] string {
"read:user" ,
"read:org" ,
"user:email" ,
} ,
} ,
AllowSignups : allowSignups ,
AllowOrganizations : allowOrgs ,
2022-07-09 02:37:18 +00:00
AllowTeams : allowTeams ,
2022-04-23 22:58:57 +00:00
AuthenticatedUser : func ( ctx context . Context , client * http . Client ) ( * github . User , error ) {
2022-08-09 01:49:51 +00:00
api , err := createClient ( client )
if err != nil {
return nil , err
}
user , _ , err := api . Users . Get ( ctx , "" )
2022-04-23 22:58:57 +00:00
return user , err
} ,
ListEmails : func ( ctx context . Context , client * http . Client ) ( [ ] * github . UserEmail , error ) {
2022-08-09 01:49:51 +00:00
api , err := createClient ( client )
if err != nil {
return nil , err
}
emails , _ , err := api . Users . ListEmails ( ctx , & github . ListOptions { } )
2022-04-23 22:58:57 +00:00
return emails , err
} ,
ListOrganizationMemberships : func ( ctx context . Context , client * http . Client ) ( [ ] * github . Membership , error ) {
2022-08-09 01:49:51 +00:00
api , err := createClient ( client )
if err != nil {
return nil , err
}
memberships , _ , err := api . Organizations . ListOrgMemberships ( ctx , & github . ListOrgMembershipsOptions {
2022-04-23 22:58:57 +00:00
State : "active" ,
2022-07-09 02:37:18 +00:00
ListOptions : github . ListOptions {
PerPage : 100 ,
} ,
2022-04-23 22:58:57 +00:00
} )
return memberships , err
} ,
2022-07-22 18:54:08 +00:00
TeamMembership : func ( ctx context . Context , client * http . Client , org , teamSlug , username string ) ( * github . Membership , error ) {
2022-08-09 01:49:51 +00:00
api , err := createClient ( client )
if err != nil {
return nil , err
}
team , _ , err := api . Teams . GetTeamMembershipBySlug ( ctx , org , teamSlug , username )
2022-07-13 00:45:43 +00:00
return team , err
2022-07-09 02:37:18 +00:00
} ,
2022-04-23 22:58:57 +00:00
} , nil
}
2022-05-03 12:48:02 +00:00
func serveHandler ( ctx context . Context , logger slog . Logger , handler http . Handler , addr , name string ) ( closeFunc func ( ) ) {
logger . Debug ( ctx , "http server listening" , slog . F ( "addr" , addr ) , slog . F ( "name" , name ) )
2022-08-21 22:32:53 +00:00
srv := & http . Server {
Addr : addr ,
Handler : handler ,
ReadHeaderTimeout : time . Minute ,
}
2022-05-03 12:48:02 +00:00
go func ( ) {
err := srv . ListenAndServe ( )
if err != nil && ! xerrors . Is ( err , http . ErrServerClosed ) {
logger . Error ( ctx , "http server listen" , slog . F ( "name" , name ) , slog . Error ( err ) )
}
} ( )
return func ( ) { _ = srv . Close ( ) }
}
2022-06-10 18:35:51 +00:00
2022-06-15 21:02:18 +00:00
// embeddedPostgresURL returns the URL for the embedded PostgreSQL deployment.
func embeddedPostgresURL ( cfg config . Root ) ( string , error ) {
pgPassword , err := cfg . PostgresPassword ( ) . Read ( )
if errors . Is ( err , os . ErrNotExist ) {
pgPassword , err = cryptorand . String ( 16 )
if err != nil {
return "" , xerrors . Errorf ( "generate password: %w" , err )
}
err = cfg . PostgresPassword ( ) . Write ( pgPassword )
if err != nil {
return "" , xerrors . Errorf ( "write password: %w" , err )
}
}
if err != nil && ! errors . Is ( err , os . ErrNotExist ) {
return "" , err
}
pgPort , err := cfg . PostgresPort ( ) . Read ( )
if errors . Is ( err , os . ErrNotExist ) {
listener , err := net . Listen ( "tcp4" , "127.0.0.1:0" )
if err != nil {
return "" , xerrors . Errorf ( "listen for random port: %w" , err )
}
_ = listener . Close ( )
tcpAddr , valid := listener . Addr ( ) . ( * net . TCPAddr )
if ! valid {
return "" , xerrors . Errorf ( "listener returned non TCP addr: %T" , tcpAddr )
}
pgPort = strconv . Itoa ( tcpAddr . Port )
err = cfg . PostgresPort ( ) . Write ( pgPort )
if err != nil {
return "" , xerrors . Errorf ( "write postgres port: %w" , err )
}
}
return fmt . Sprintf ( "postgres://coder@localhost:%s/coder?sslmode=disable&password=%s" , pgPort , pgPassword ) , nil
}
func startBuiltinPostgres ( ctx context . Context , cfg config . Root , logger slog . Logger ) ( string , func ( ) error , error ) {
usr , err := user . Current ( )
if err != nil {
return "" , nil , err
}
if usr . Uid == "0" {
return "" , nil , xerrors . New ( "The built-in PostgreSQL cannot run as the root user. Create a non-root user and run again!" )
}
// Ensure a password and port have been generated!
connectionURL , err := embeddedPostgresURL ( cfg )
if err != nil {
return "" , nil , err
}
pgPassword , err := cfg . PostgresPassword ( ) . Read ( )
if err != nil {
return "" , nil , xerrors . Errorf ( "read postgres password: %w" , err )
}
pgPortRaw , err := cfg . PostgresPort ( ) . Read ( )
if err != nil {
return "" , nil , xerrors . Errorf ( "read postgres port: %w" , err )
}
pgPort , err := strconv . Atoi ( pgPortRaw )
if err != nil {
return "" , nil , xerrors . Errorf ( "parse postgres port: %w" , err )
}
stdlibLogger := slog . Stdlib ( ctx , logger . Named ( "postgres" ) , slog . LevelDebug )
ep := embeddedpostgres . NewDatabase (
embeddedpostgres . DefaultConfig ( ) .
2022-08-10 15:08:24 +00:00
Version ( embeddedpostgres . V13 ) .
2022-06-15 21:02:18 +00:00
BinariesPath ( filepath . Join ( cfg . PostgresPath ( ) , "bin" ) ) .
DataPath ( filepath . Join ( cfg . PostgresPath ( ) , "data" ) ) .
RuntimePath ( filepath . Join ( cfg . PostgresPath ( ) , "runtime" ) ) .
CachePath ( filepath . Join ( cfg . PostgresPath ( ) , "cache" ) ) .
Username ( "coder" ) .
Password ( pgPassword ) .
Database ( "coder" ) .
Port ( uint32 ( pgPort ) ) .
Logger ( stdlibLogger . Writer ( ) ) ,
)
err = ep . Start ( )
if err != nil {
return "" , nil , xerrors . Errorf ( "Failed to start built-in PostgreSQL. Optionally, specify an external deployment with `--postgres-url`: %w" , err )
}
return connectionURL , ep . Stop , nil
}