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-03-22 19:17:50 +00:00
"time"
2022-06-01 10:00:42 +00:00
"github.com/coder/coder/buildinfo"
2022-06-15 21:02:18 +00:00
"github.com/coder/coder/cryptorand"
2022-05-19 22:47:45 +00:00
"github.com/coder/coder/provisioner/echo"
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-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-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-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-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"
"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
oauth2GithubAllowSignups bool
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-04-18 17:37:01 +00:00
logger := slog . Make ( sloghuman . Sink ( os . Stderr ) )
2022-06-15 21:02:18 +00:00
if verbose {
2022-05-03 21:13:30 +00:00
logger = logger . Leveled ( slog . LevelDebug )
}
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 {
tracerProvider , err = tracing . TracerProvider ( cmd . Context ( ) , "coderd" )
if err != nil {
logger . Warn ( cmd . Context ( ) , "failed to start telemetry exporter" , slog . Error ( err ) )
} else {
defer func ( ) {
2022-05-19 23:28:29 +00:00
// allow time for traces to flush even if command context is canceled
2022-05-19 22:43:07 +00:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , 5 * time . Second )
defer cancel ( )
_ = tracerProvider . Shutdown ( ctx )
} ( )
2022-05-20 15:51:06 +00:00
d , err := tracing . PostgresDriver ( tracerProvider , "coderd.database" )
if err != nil {
logger . Warn ( cmd . Context ( ) , "failed to start postgres tracing driver" , slog . Error ( err ) )
} 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 )
// 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 ( ) )
postgresURL , closeFunc , err = startBuiltinPostgres ( cmd . Context ( ) , config , logger )
if err != nil {
return err
}
defer func ( ) {
// Gracefully shut PostgreSQL down!
_ = closeFunc ( )
} ( )
}
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 (
ctxTunnel , closeTunnel = context . WithCancel ( cmd . Context ( ) )
2022-06-10 18:38:11 +00:00
devTunnel = ( * devtunnel . Tunnel ) ( nil )
devTunnelErrChan = make ( <- chan error , 1 )
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" )
devTunnel , devTunnelErrChan , err = devtunnel . New ( ctxTunnel , logger . Named ( "devtunnel" ) )
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-06-10 18:35:51 +00:00
// Warn the user if the access URL appears to be a loopback address.
isLocal , err := isLocalURL ( cmd . Context ( ) , accessURL )
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-06-15 21:02:18 +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 ( accessURL ) , reason )
cmd . Println ( cliui . Styles . Code . Render ( strings . Join ( os . Args , " " ) + " --tunnel" ) )
2022-03-22 19:17:50 +00:00
}
2022-06-15 21:02:18 +00:00
cmd . Printf ( "View the Web UI: %s\n" , accessURL )
2022-03-24 15:07:33 +00:00
2022-03-24 19:21:05 +00:00
accessURLParsed , err := url . Parse ( accessURL )
if err != nil {
return xerrors . Errorf ( "parse access url %q: %w" , accessURL , err )
}
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.
googleTokenValidator , err := idtoken . NewValidator ( cmd . Context ( ) , option . WithoutAuthentication ( ) )
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-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-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-03-24 15:07:33 +00:00
}
2022-04-23 22:58:57 +00:00
if oauth2GithubClientSecret != "" {
options . GithubOAuth2Config , err = configureGithubOAuth2 ( accessURLParsed , oauth2GithubClientID , oauth2GithubClientSecret , oauth2GithubAllowSignups , oauth2GithubAllowedOrganizations )
if err != nil {
return xerrors . Errorf ( "configure github oauth2: %w" , err )
}
}
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 )
}
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 )
options . Pubsub , err = database . NewPubsub ( cmd . Context ( ) , sqlDB , postgresURL )
if err != nil {
return xerrors . Errorf ( "create pubsub: %w" , err )
}
}
2022-05-26 03:14:08 +00:00
coderAPI := coderd . New ( options )
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-05-03 12:48:02 +00:00
// This prevents the pprof import from being accidentally deleted.
var _ = pprof . Handler
if pprofEnabled {
//nolint:revive
defer serveHandler ( cmd . Context ( ) , logger , nil , pprofAddress , "pprof" ) ( )
}
if promEnabled {
//nolint:revive
defer serveHandler ( cmd . Context ( ) , logger , promhttp . Handler ( ) , promAddress , "prometheus" ) ( )
}
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-03-28 19:26:41 +00:00
for i := 0 ; uint8 ( i ) < provisionerDaemonCount ; i ++ {
2022-06-15 21:02:18 +00:00
daemonClose , err := newProvisionerDaemon ( cmd . Context ( ) , coderAPI , logger , cacheDir , errCh , false )
2022-03-24 15:07:33 +00:00
if err != nil {
return xerrors . Errorf ( "create provisioner daemon: %w" , err )
}
provisionerDaemons = append ( provisionerDaemons , daemonClose )
2022-03-22 19:17:50 +00:00
}
2022-03-24 15:07:33 +00:00
defer func ( ) {
for _ , provisionerDaemon := range provisionerDaemons {
_ = provisionerDaemon . Close ( )
}
} ( )
2022-03-22 19:17:50 +00:00
2022-03-24 19:21:05 +00:00
shutdownConnsCtx , shutdownConns := context . WithCancel ( cmd . Context ( ) )
defer shutdownConns ( )
2022-03-22 19:17:50 +00:00
go func ( ) {
defer close ( errCh )
2022-03-24 19:21:05 +00:00
server := http . Server {
2022-05-02 01:31:12 +00:00
// These errors are typically noise like "TLS: EOF". Vault does similar:
// https://github.com/hashicorp/vault/blob/e2490059d0711635e529a4efcbaa1b26998d6e1c/command/server.go#L2714
ErrorLog : log . New ( io . Discard , "" , 0 ) ,
2022-05-26 03:14:08 +00:00
Handler : coderAPI . Handler ,
2022-03-24 19:21:05 +00:00
BaseContext : func ( _ net . Listener ) context . Context {
return shutdownConnsCtx
} ,
}
2022-06-10 18:38:11 +00:00
wg := errgroup . Group { }
wg . Go ( func ( ) error {
// Make sure to close the tunnel listener if we exit so the
// errgroup doesn't wait forever!
2022-06-15 21:02:18 +00:00
if tunnel {
2022-06-10 18:38:11 +00:00
defer devTunnel . Listener . Close ( )
}
return server . Serve ( listener )
} )
2022-06-15 21:02:18 +00:00
if tunnel {
2022-06-10 18:38:11 +00:00
wg . Go ( func ( ) error {
defer listener . Close ( )
return server . Serve ( devTunnel . Listener )
} )
}
errCh <- wg . Wait ( )
2022-03-22 19:17:50 +00:00
} ( )
2022-06-15 21:02:18 +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 ( ) )
2022-03-24 15:07:33 +00:00
2022-06-15 21:02:18 +00:00
hasFirstUser , err := client . HasFirstUser ( cmd . Context ( ) )
if ! hasFirstUser && err == nil {
cmd . Println ( )
cmd . Println ( "Get started by creating the first user (in a new terminal):" )
cmd . Println ( cliui . Styles . Code . Render ( "coder login " + accessURL ) )
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 ( )
autobuildExecutor := executor . New ( cmd . Context ( ) , options . Database , logger , autobuildPoller . C )
autobuildExecutor . Run ( )
2022-05-11 22:03:02 +00:00
2022-05-06 20:45:18 +00:00
// Because the graceful shutdown includes cleaning up workspaces in dev mode, we're
// going to make it harder to accidentally skip the graceful shutdown by hitting ctrl+c
// two or more times. So the stopChan is unlimited in size and we don't call
// signal.Stop() until graceful shutdown finished--this means we swallow additional
// SIGINT after the first. To get out of a graceful shutdown, the user can send SIGQUIT
// with ctrl+\ or SIGTERM with `kill`.
2022-03-24 15:07:33 +00:00
stopChan := make ( chan os . Signal , 1 )
defer signal . Stop ( stopChan )
signal . Notify ( stopChan , os . Interrupt )
2022-03-22 19:17:50 +00:00
select {
case <- cmd . Context ( ) . Done ( ) :
2022-05-26 03:14:08 +00:00
coderAPI . Close ( )
2022-03-22 19:17:50 +00:00
return cmd . Context ( ) . Err ( )
2022-06-10 18:38:11 +00:00
case err := <- devTunnelErrChan :
2022-04-14 15:29:40 +00:00
if err != nil {
return err
}
2022-03-22 19:17:50 +00:00
case err := <- errCh :
2022-05-10 02:19:20 +00:00
shutdownConns ( )
2022-05-26 03:14:08 +00:00
coderAPI . Close ( )
2022-03-22 19:17:50 +00:00
return err
2022-03-24 15:07:33 +00:00
case <- stopChan :
}
_ , err = daemon . SdNotify ( false , daemon . SdNotifyStopping )
if err != nil {
return xerrors . Errorf ( "notify systemd: %w" , err )
}
2022-06-15 21:02:18 +00:00
_ , _ = fmt . Fprintln ( cmd . OutOrStdout ( ) , cliui . Styles . Bold . Render (
"Interrupt caught, gracefully exiting. Use ctrl+\\ to force quit" ) )
2022-03-24 15:07:33 +00:00
for _ , provisionerDaemon := range provisionerDaemons {
2022-06-15 21:02:18 +00:00
if verbose {
cmd . Println ( "Shutting down provisioner daemon..." )
}
2022-03-24 15:07:33 +00:00
err = provisionerDaemon . Shutdown ( cmd . Context ( ) )
if err != nil {
2022-06-15 21:02:18 +00:00
cmd . PrintErrf ( "Failed to shutdown provisioner daemon: %s\n" , err )
continue
2022-03-24 15:07:33 +00:00
}
err = provisionerDaemon . Close ( )
if err != nil {
return xerrors . Errorf ( "close provisioner daemon: %w" , err )
}
2022-06-15 21:02:18 +00:00
if verbose {
cmd . Println ( "Gracefully shut down provisioner daemon!" )
}
2022-03-22 19:17:50 +00:00
}
2022-03-24 15:07:33 +00:00
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-06-10 18:38:11 +00:00
<- devTunnelErrChan
2022-04-14 15:29:40 +00:00
}
2022-06-15 21:02:18 +00:00
cmd . Println ( "Waiting for WebSocket connections to close..." )
2022-03-24 19:21:05 +00:00
shutdownConns ( )
2022-05-26 03:14:08 +00:00
coderAPI . Close ( )
2022-03-24 15:07:33 +00:00
return nil
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-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." )
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-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-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-05-19 22:47:45 +00:00
logger slog . Logger , cacheDir string , errChan chan error , dev bool ) ( * provisionerd . Server , error ) {
2022-03-30 22:59:54 +00:00
err := os . MkdirAll ( cacheDir , 0700 )
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 ( ) {
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
} )
if err != nil {
2022-04-18 20:11:59 +00:00
errChan <- err
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-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 {
errChan <- err
}
} ( )
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-04-23 22:58:57 +00:00
func configureGithubOAuth2 ( accessURL * url . URL , clientID , clientSecret string , allowSignups bool , allowOrgs [ ] string ) ( * coderd . GithubOAuth2Config , error ) {
redirectURL , err := accessURL . Parse ( "/api/v2/users/oauth2/github/callback" )
if err != nil {
return nil , xerrors . Errorf ( "parse github oauth callback url: %w" , err )
}
return & coderd . GithubOAuth2Config {
OAuth2Config : & oauth2 . Config {
ClientID : clientID ,
ClientSecret : clientSecret ,
Endpoint : xgithub . Endpoint ,
RedirectURL : redirectURL . String ( ) ,
Scopes : [ ] string {
"read:user" ,
"read:org" ,
"user:email" ,
} ,
} ,
AllowSignups : allowSignups ,
AllowOrganizations : allowOrgs ,
AuthenticatedUser : func ( ctx context . Context , client * http . Client ) ( * github . User , error ) {
user , _ , err := github . NewClient ( client ) . Users . Get ( ctx , "" )
return user , err
} ,
ListEmails : func ( ctx context . Context , client * http . Client ) ( [ ] * github . UserEmail , error ) {
emails , _ , err := github . NewClient ( client ) . Users . ListEmails ( ctx , & github . ListOptions { } )
return emails , err
} ,
ListOrganizationMemberships : func ( ctx context . Context , client * http . Client ) ( [ ] * github . Membership , error ) {
memberships , _ , err := github . NewClient ( client ) . Organizations . ListOrgMemberships ( ctx , & github . ListOrgMembershipsOptions {
State : "active" ,
} )
return memberships , err
} ,
} , 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 ) )
srv := & http . Server { Addr : addr , Handler : handler }
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
// isLocalURL returns true if the hostname of the provided URL appears to
// resolve to a loopback address.
func isLocalURL ( ctx context . Context , urlString string ) ( bool , error ) {
parsedURL , err := url . Parse ( urlString )
if err != nil {
return false , err
}
resolver := & net . Resolver { }
ips , err := resolver . LookupIPAddr ( ctx , parsedURL . Hostname ( ) )
if err != nil {
return false , err
}
for _ , ip := range ips {
if ip . IP . IsLoopback ( ) {
return true , nil
}
}
return false , nil
}
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 ( ) .
Version ( embeddedpostgres . V13 ) .
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
}