2023-01-17 16:58:00 +00:00
//go:build !slim
2022-03-22 19:17:50 +00:00
package cli
import (
"context"
2023-02-02 17:08:35 +00:00
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
2022-03-24 19:21:05 +00:00
"crypto/tls"
"crypto/x509"
2022-03-24 15:07:33 +00:00
"database/sql"
2023-03-07 19:38:11 +00:00
"encoding/hex"
2022-04-11 23:54:30 +00:00
"errors"
2023-03-07 21:10:01 +00:00
"flag"
2022-03-22 19:17:50 +00:00
"fmt"
2022-05-02 01:31:12 +00:00
"io"
"log"
2023-02-02 17:08:35 +00:00
"math/big"
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-06-15 21:02:18 +00:00
"os/user"
2022-03-28 19:57:19 +00:00
"path/filepath"
2022-10-14 18:25:11 +00:00
"regexp"
2023-03-07 21:10:01 +00:00
"sort"
2022-06-15 21:02:18 +00:00
"strconv"
"strings"
2022-07-27 15:21:21 +00:00
"sync"
2023-04-04 12:48:35 +00:00
"sync/atomic"
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-08-08 15:09:46 +00:00
"github.com/prometheus/client_golang/prometheus"
2022-11-05 00:03:01 +00:00
"github.com/prometheus/client_golang/prometheus/collectors"
2022-05-03 12:48:02 +00:00
"github.com/prometheus/client_golang/prometheus/promhttp"
2023-10-18 20:08:02 +00:00
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
2022-09-16 16:43:22 +00:00
"go.opentelemetry.io/otel/trace"
2022-10-18 01:02:25 +00:00
"golang.org/x/mod/semver"
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"
2023-03-07 21:10:01 +00:00
"gopkg.in/yaml.v3"
2022-09-01 01:09:44 +00:00
"tailscale.com/tailcfg"
2022-05-19 22:43:07 +00:00
2022-03-22 19:17:50 +00:00
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/buildinfo"
2023-12-19 16:49:50 +00:00
"github.com/coder/coder/v2/cli/clilog"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/cli/cliui"
2023-12-07 16:59:13 +00:00
"github.com/coder/coder/v2/cli/cliutil"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/cli/config"
"github.com/coder/coder/v2/coderd"
"github.com/coder/coder/v2/coderd/autobuild"
"github.com/coder/coder/v2/coderd/batchstats"
"github.com/coder/coder/v2/coderd/database"
2024-03-20 17:14:43 +00:00
"github.com/coder/coder/v2/coderd/database/awsiamrds"
2023-10-30 17:42:20 +00:00
"github.com/coder/coder/v2/coderd/database/dbmem"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/coderd/database/dbmetrics"
"github.com/coder/coder/v2/coderd/database/dbpurge"
"github.com/coder/coder/v2/coderd/database/migrations"
"github.com/coder/coder/v2/coderd/database/pubsub"
"github.com/coder/coder/v2/coderd/devtunnel"
2023-09-30 19:30:01 +00:00
"github.com/coder/coder/v2/coderd/externalauth"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/coderd/gitsshkey"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/oauthpki"
"github.com/coder/coder/v2/coderd/prometheusmetrics"
2023-10-19 08:45:12 +00:00
"github.com/coder/coder/v2/coderd/prometheusmetrics/insights"
2024-01-10 15:13:30 +00:00
"github.com/coder/coder/v2/coderd/promoauth"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/coderd/schedule"
"github.com/coder/coder/v2/coderd/telemetry"
"github.com/coder/coder/v2/coderd/tracing"
"github.com/coder/coder/v2/coderd/unhanger"
"github.com/coder/coder/v2/coderd/updatecheck"
"github.com/coder/coder/v2/coderd/util/slice"
2023-12-07 16:59:13 +00:00
stringutil "github.com/coder/coder/v2/coderd/util/strings"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/coderd/workspaceapps"
2024-01-17 16:41:42 +00:00
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
2024-03-20 16:44:12 +00:00
"github.com/coder/coder/v2/coderd/workspaceusage"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/codersdk"
2023-12-15 08:41:39 +00:00
"github.com/coder/coder/v2/codersdk/drpc"
2023-08-18 18:55:43 +00:00
"github.com/coder/coder/v2/cryptorand"
"github.com/coder/coder/v2/provisioner/echo"
"github.com/coder/coder/v2/provisioner/terraform"
"github.com/coder/coder/v2/provisionerd"
"github.com/coder/coder/v2/provisionerd/proto"
"github.com/coder/coder/v2/provisionersdk"
sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
"github.com/coder/coder/v2/tailnet"
2024-01-17 16:41:42 +00:00
"github.com/coder/pretty"
2023-04-28 18:51:31 +00:00
"github.com/coder/retry"
2024-03-15 16:24:38 +00:00
"github.com/coder/serpent"
2023-03-22 13:13:48 +00:00
"github.com/coder/wgtunnel/tunnelsdk"
2022-03-22 19:17:50 +00:00
)
2023-08-27 19:46:44 +00:00
func createOIDCConfig ( ctx context . Context , vals * codersdk . DeploymentValues ) ( * coderd . OIDCConfig , error ) {
if vals . OIDC . ClientID == "" {
return nil , xerrors . Errorf ( "OIDC client ID must be set!" )
}
if vals . OIDC . IssuerURL == "" {
return nil , xerrors . Errorf ( "OIDC issuer URL must be set!" )
}
oidcProvider , err := oidc . NewProvider (
ctx , vals . OIDC . IssuerURL . String ( ) ,
)
if err != nil {
return nil , xerrors . Errorf ( "configure oidc provider: %w" , err )
}
redirectURL , err := vals . AccessURL . Value ( ) . Parse ( "/api/v2/users/oidc/callback" )
if err != nil {
return nil , xerrors . Errorf ( "parse oidc oauth callback url: %w" , err )
}
// If the scopes contain 'groups', we enable group support.
// Do not override any custom value set by the user.
if slice . Contains ( vals . OIDC . Scopes , "groups" ) && vals . OIDC . GroupField == "" {
vals . OIDC . GroupField = "groups"
}
oauthCfg := & oauth2 . Config {
ClientID : vals . OIDC . ClientID . String ( ) ,
ClientSecret : vals . OIDC . ClientSecret . String ( ) ,
RedirectURL : redirectURL . String ( ) ,
Endpoint : oidcProvider . Endpoint ( ) ,
Scopes : vals . OIDC . Scopes ,
}
2024-01-10 15:13:30 +00:00
var useCfg promoauth . OAuth2Config = oauthCfg
2023-08-27 19:46:44 +00:00
if vals . OIDC . ClientKeyFile != "" {
// PKI authentication is done in the params. If a
// counter example is found, we can add a config option to
// change this.
oauthCfg . Endpoint . AuthStyle = oauth2 . AuthStyleInParams
if vals . OIDC . ClientSecret != "" {
return nil , xerrors . Errorf ( "cannot specify both oidc client secret and oidc client key file" )
}
pkiCfg , err := configureOIDCPKI ( oauthCfg , vals . OIDC . ClientKeyFile . Value ( ) , vals . OIDC . ClientCertFile . Value ( ) )
if err != nil {
return nil , xerrors . Errorf ( "configure oauth pki authentication: %w" , err )
}
useCfg = pkiCfg
}
2023-12-08 16:14:19 +00:00
if len ( vals . OIDC . GroupAllowList ) > 0 && vals . OIDC . GroupField == "" {
return nil , xerrors . Errorf ( "'oidc-group-field' must be set if 'oidc-allowed-groups' is set. Either unset 'oidc-allowed-groups' or set 'oidc-group-field'" )
}
groupAllowList := make ( map [ string ] bool )
for _ , group := range vals . OIDC . GroupAllowList . Value ( ) {
groupAllowList [ group ] = true
}
2023-08-27 19:46:44 +00:00
return & coderd . OIDCConfig {
OAuth2Config : useCfg ,
Provider : oidcProvider ,
Verifier : oidcProvider . Verifier ( & oidc . Config {
ClientID : vals . OIDC . ClientID . String ( ) ,
} ) ,
EmailDomain : vals . OIDC . EmailDomain ,
AllowSignups : vals . OIDC . AllowSignups . Value ( ) ,
UsernameField : vals . OIDC . UsernameField . String ( ) ,
EmailField : vals . OIDC . EmailField . String ( ) ,
AuthURLParams : vals . OIDC . AuthURLParams . Value ,
IgnoreUserInfo : vals . OIDC . IgnoreUserInfo . Value ( ) ,
GroupField : vals . OIDC . GroupField . String ( ) ,
GroupFilter : vals . OIDC . GroupRegexFilter . Value ( ) ,
2023-12-08 16:14:19 +00:00
GroupAllowList : groupAllowList ,
2023-08-27 19:46:44 +00:00
CreateMissingGroups : vals . OIDC . GroupAutoCreate . Value ( ) ,
GroupMapping : vals . OIDC . GroupMapping . Value ,
UserRoleField : vals . OIDC . UserRoleField . String ( ) ,
UserRoleMapping : vals . OIDC . UserRoleMapping . Value ,
UserRolesDefault : vals . OIDC . UserRolesDefault . GetSlice ( ) ,
SignInText : vals . OIDC . SignInText . String ( ) ,
2024-02-01 17:01:25 +00:00
SignupsDisabledText : vals . OIDC . SignupsDisabledText . String ( ) ,
2023-08-27 19:46:44 +00:00
IconURL : vals . OIDC . IconURL . String ( ) ,
IgnoreEmailVerified : vals . OIDC . IgnoreEmailVerified . Value ( ) ,
} , nil
}
func afterCtx ( ctx context . Context , fn func ( ) ) {
go func ( ) {
<- ctx . Done ( )
fn ( )
} ( )
}
func enablePrometheus (
ctx context . Context ,
logger slog . Logger ,
vals * codersdk . DeploymentValues ,
options * coderd . Options ,
) ( closeFn func ( ) , err error ) {
options . PrometheusRegistry . MustRegister ( collectors . NewGoCollector ( ) )
options . PrometheusRegistry . MustRegister ( collectors . NewProcessCollector ( collectors . ProcessCollectorOpts { } ) )
closeUsersFunc , err := prometheusmetrics . ActiveUsers ( ctx , options . PrometheusRegistry , options . Database , 0 )
if err != nil {
return nil , xerrors . Errorf ( "register active users prometheus metric: %w" , err )
}
afterCtx ( ctx , closeUsersFunc )
2024-04-02 07:57:36 +00:00
closeWorkspacesFunc , err := prometheusmetrics . Workspaces ( ctx , options . Logger . Named ( "workspaces_metrics" ) , options . PrometheusRegistry , options . Database , 0 )
2023-08-27 19:46:44 +00:00
if err != nil {
return nil , xerrors . Errorf ( "register workspaces prometheus metric: %w" , err )
}
afterCtx ( ctx , closeWorkspacesFunc )
2023-10-19 08:45:12 +00:00
insightsMetricsCollector , err := insights . NewMetricsCollector ( options . Database , options . Logger , 0 , 0 )
if err != nil {
return nil , xerrors . Errorf ( "unable to initialize insights metrics collector: %w" , err )
}
err = options . PrometheusRegistry . Register ( insightsMetricsCollector )
if err != nil {
return nil , xerrors . Errorf ( "unable to register insights metrics collector: %w" , err )
}
closeInsightsMetricsCollector , err := insightsMetricsCollector . Run ( ctx )
if err != nil {
return nil , xerrors . Errorf ( "unable to run insights metrics collector: %w" , err )
}
afterCtx ( ctx , closeInsightsMetricsCollector )
2023-08-27 19:46:44 +00:00
if vals . Prometheus . CollectAgentStats {
2024-03-13 10:03:36 +00:00
closeAgentStatsFunc , err := prometheusmetrics . AgentStats ( ctx , logger , options . PrometheusRegistry , options . Database , time . Now ( ) , 0 , options . DeploymentValues . Prometheus . AggregateAgentStatsBy . Value ( ) )
2023-08-27 19:46:44 +00:00
if err != nil {
return nil , xerrors . Errorf ( "register agent stats prometheus metric: %w" , err )
}
afterCtx ( ctx , closeAgentStatsFunc )
2024-03-13 10:03:36 +00:00
metricsAggregator , err := prometheusmetrics . NewMetricsAggregator ( logger , options . PrometheusRegistry , 0 , options . DeploymentValues . Prometheus . AggregateAgentStatsBy . Value ( ) )
2023-08-27 19:46:44 +00:00
if err != nil {
return nil , xerrors . Errorf ( "can't initialize metrics aggregator: %w" , err )
}
cancelMetricsAggregator := metricsAggregator . Run ( ctx )
afterCtx ( ctx , cancelMetricsAggregator )
options . UpdateAgentMetrics = metricsAggregator . Update
err = options . PrometheusRegistry . Register ( metricsAggregator )
if err != nil {
return nil , xerrors . Errorf ( "can't register metrics aggregator as collector: %w" , err )
}
}
//nolint:revive
return ServeHandler (
ctx , logger , promhttp . InstrumentMetricHandler (
options . PrometheusRegistry , promhttp . HandlerFor ( options . PrometheusRegistry , promhttp . HandlerOpts { } ) ,
) , vals . Prometheus . Address . String ( ) , "prometheus" ,
) , nil
}
2024-03-19 12:11:27 +00:00
//nolint:gocognit // TODO(dannyk): reduce complexity of this function
2024-03-17 14:45:26 +00:00
func ( r * RootCmd ) Server ( newAPI func ( context . Context , * coderd . Options ) ( * coderd . API , io . Closer , error ) ) * serpent . Command {
2023-09-01 13:32:21 +00:00
if newAPI == nil {
newAPI = func ( _ context . Context , o * coderd . Options ) ( * coderd . API , io . Closer , error ) {
api := coderd . New ( o )
return api , api , nil
}
}
2023-03-23 22:42:20 +00:00
var (
2023-08-27 19:46:44 +00:00
vals = new ( codersdk . DeploymentValues )
opts = vals . Options ( )
2023-03-23 22:42:20 +00:00
)
2024-03-17 14:45:26 +00:00
serverCmd := & serpent . Command {
2023-04-11 19:47:07 +00:00
Use : "server" ,
Short : "Start a Coder server" ,
Options : opts ,
2024-03-15 16:24:38 +00:00
Middleware : serpent . Chain (
2023-08-27 19:46:44 +00:00
WriteConfigMW ( vals ) ,
2023-04-13 14:07:19 +00:00
PrintDeprecatedOptions ( ) ,
2024-03-15 16:24:38 +00:00
serpent . RequireNArgs ( 0 ) ,
2023-04-11 19:47:07 +00:00
) ,
2024-03-15 16:24:38 +00:00
Handler : func ( inv * serpent . Invocation ) error {
2023-01-11 16:22:20 +00:00
// Main command context for managing cancellation of running
// services.
2023-03-23 22:42:20 +00:00
ctx , cancel := context . WithCancel ( inv . Context ( ) )
2023-01-11 16:22:20 +00:00
defer cancel ( )
2023-08-27 19:46:44 +00:00
if vals . Config != "" {
2023-04-07 22:58:21 +00:00
cliui . Warnf ( inv . Stderr , "YAML support is experimental and offers no compatibility guarantees." )
}
2024-03-04 16:35:33 +00:00
go DumpHandler ( ctx , "coderd" )
2023-03-07 21:10:01 +00:00
2022-12-15 20:09:19 +00:00
// Validate bind addresses.
2023-08-27 19:46:44 +00:00
if vals . Address . String ( ) != "" {
if vals . TLS . Enable {
vals . HTTPAddress = ""
vals . TLS . Address = vals . Address
2022-12-15 20:09:19 +00:00
} else {
2023-08-27 19:46:44 +00:00
_ = vals . HTTPAddress . Set ( vals . Address . String ( ) )
vals . TLS . Address . Host = ""
vals . TLS . Address . Port = ""
2022-12-15 20:09:19 +00:00
}
}
2023-08-27 19:46:44 +00:00
if vals . TLS . Enable && vals . TLS . Address . String ( ) == "" {
2022-12-15 20:09:19 +00:00
return xerrors . Errorf ( "TLS address must be set if TLS is enabled" )
}
2023-08-27 19:46:44 +00:00
if ! vals . TLS . Enable && vals . HTTPAddress . String ( ) == "" {
2023-01-18 23:23:55 +00:00
return xerrors . Errorf ( "TLS is disabled. Enable with --tls-enable or specify a HTTP address" )
2022-12-15 20:09:19 +00:00
}
2023-08-27 19:46:44 +00:00
if vals . AccessURL . String ( ) != "" &&
! ( vals . AccessURL . Scheme == "http" || vals . AccessURL . Scheme == "https" ) {
2023-03-07 21:10:01 +00:00
return xerrors . Errorf ( "access-url must include a scheme (e.g. 'http://' or 'https://)" )
}
2023-01-05 18:05:20 +00:00
// Disable rate limits if the `--dangerous-disable-rate-limits` flag
// was specified.
loginRateLimit := 60
filesRateLimit := 12
2023-08-27 19:46:44 +00:00
if vals . RateLimit . DisableAll {
vals . RateLimit . API = - 1
2023-01-05 18:05:20 +00:00
loginRateLimit = - 1
filesRateLimit = - 1
}
2023-07-12 12:07:36 +00:00
PrintLogo ( inv , "Coder" )
2023-12-19 16:49:50 +00:00
logger , logCloser , err := clilog . New ( clilog . FromDeploymentValues ( vals ) ) . Build ( inv )
2023-01-13 02:08:23 +00:00
if err != nil {
return xerrors . Errorf ( "make logger: %w" , err )
2022-11-09 18:58:23 +00:00
}
2023-01-13 02:08:23 +00:00
defer logCloser ( )
2022-05-03 21:13:30 +00:00
2023-03-07 21:10:01 +00:00
// This line is helpful in tests.
logger . Debug ( ctx , "started debug logging" )
logger . Sync ( )
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
2024-03-15 13:16:36 +00:00
// shadowing cancel() (from above) here because stopCancel()
2022-08-18 13:25:32 +00:00
// restores default behavior for the signals. This protects
2022-10-25 00:46:24 +00:00
// the shutdown sequence from abruptly terminating things
2022-08-18 13:25:32 +00:00
// 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`.
2024-03-15 13:16:36 +00:00
stopCtx , stopCancel := signalNotifyContext ( ctx , inv , StopSignalsNoInterrupt ... )
defer stopCancel ( )
interruptCtx , interruptCancel := signalNotifyContext ( ctx , inv , InterruptSignals ... )
defer interruptCancel ( )
2022-08-18 13:25:32 +00:00
2023-08-27 19:46:44 +00:00
cacheDir := vals . CacheDir . String ( )
2022-12-06 17:05:14 +00:00
err = os . MkdirAll ( cacheDir , 0 o700 )
if err != nil {
return xerrors . Errorf ( "create cache directory: %w" , err )
}
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 ( )
2023-09-14 06:20:28 +00:00
tracerProvider , sqlDriver , closeTracing := ConfigureTraceProvider ( ctx , logger , vals )
2023-05-02 17:06:58 +00:00
defer func ( ) {
logger . Debug ( ctx , "closing tracing" )
traceCloseErr := shutdownWithTimeout ( closeTracing , 5 * time . Second )
logger . Debug ( ctx , "tracing closed" , slog . Error ( traceCloseErr ) )
} ( )
2023-11-07 14:55:39 +00:00
httpServers , err := ConfigureHTTPServers ( logger , inv , vals )
2023-04-13 14:07:19 +00:00
if err != nil {
return xerrors . Errorf ( "configure http(s): %w" , err )
2022-03-28 22:11:52 +00:00
}
2023-04-13 14:07:19 +00:00
defer httpServers . Close ( )
2022-03-24 15:07:33 +00:00
2023-03-23 22:42:20 +00:00
config := r . createConfig ( )
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!
2023-08-27 19:46:44 +00:00
if ! vals . InMemoryDatabase && vals . PostgresURL == "" {
2022-06-15 21:02:18 +00:00
var closeFunc func ( ) error
2023-03-23 22:42:20 +00:00
cliui . Infof ( inv . Stdout , "Using built-in PostgreSQL (%s)" , config . PostgresPath ( ) )
2023-03-07 21:10:01 +00:00
pgURL , closeFunc , err := startBuiltinPostgres ( ctx , config , logger )
if err != nil {
return err
}
2023-08-27 19:46:44 +00:00
err = vals . PostgresURL . Set ( pgURL )
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 ( ) {
2023-03-23 22:42:20 +00:00
cliui . Infof ( inv . Stdout , "Stopping built-in PostgreSQL..." )
2022-06-15 21:02:18 +00:00
// Gracefully shut PostgreSQL down!
2022-11-29 13:45:14 +00:00
if err := closeFunc ( ) ; err != nil {
2023-03-23 22:42:20 +00:00
cliui . Errorf ( inv . Stderr , "Failed to stop built-in PostgreSQL: %v" , err )
2022-11-29 13:45:14 +00:00
} else {
2023-03-23 22:42:20 +00:00
cliui . Infof ( inv . Stdout , "Stopped built-in PostgreSQL" )
2022-11-29 13:45:14 +00:00
}
2022-06-15 21:02:18 +00:00
} ( )
}
2022-12-15 20:09:19 +00:00
// Prefer HTTP because it's less prone to TLS errors over localhost.
2023-04-13 14:07:19 +00:00
localURL := httpServers . TLSUrl
if httpServers . HTTPUrl != nil {
localURL = httpServers . HTTPUrl
2022-03-24 19:21:05 +00:00
}
2023-04-13 14:07:19 +00:00
ctx , httpClient , err := ConfigureHTTPClient (
2022-12-14 14:44:29 +00:00
ctx ,
2023-08-27 19:46:44 +00:00
vals . TLS . ClientCertFile . String ( ) ,
vals . TLS . ClientKeyFile . String ( ) ,
vals . TLS . ClientCAFile . String ( ) ,
2022-12-14 14:44:29 +00:00
)
if err != nil {
return xerrors . Errorf ( "configure http client: %w" , err )
}
2022-10-20 22:09:44 +00:00
// If the access URL is empty, we attempt to run a reverse-proxy
// tunnel to make the initial setup really simple.
2023-03-22 13:13:48 +00:00
var (
tunnel * tunnelsdk . Tunnel
tunnelDone <- chan struct { } = make ( chan struct { } , 1 )
)
2023-08-27 19:46:44 +00:00
if vals . AccessURL . String ( ) == "" {
2023-03-28 01:01:25 +00:00
cliui . Infof ( inv . Stderr , "Opening tunnel so workspaces can connect to your deployment. For production scenarios, specify an external access URL" )
2023-08-27 19:46:44 +00:00
tunnel , err = devtunnel . New ( ctx , logger . Named ( "net.devtunnel" ) , vals . WgtunnelHost . String ( ) )
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
}
2023-03-22 13:13:48 +00:00
defer tunnel . Close ( )
tunnelDone = tunnel . Wait ( )
2024-03-15 16:24:38 +00:00
vals . AccessURL = serpent . URL ( * tunnel . URL )
2022-10-20 22:09:44 +00:00
2023-08-27 19:46:44 +00:00
if vals . WildcardAccessURL . String ( ) == "" {
2022-10-20 22:09:44 +00:00
// Suffixed wildcard access URL.
2024-01-17 16:41:42 +00:00
wu := fmt . Sprintf ( "*--%s" , tunnel . URL . Hostname ( ) )
err = vals . WildcardAccessURL . Set ( wu )
2023-03-07 21:10:01 +00:00
if err != nil {
2024-01-17 16:41:42 +00:00
return xerrors . Errorf ( "set wildcard access url %q: %w" , wu , err )
2023-03-07 21:10:01 +00:00
}
2022-10-20 22:09:44 +00:00
}
2022-03-22 19:17:50 +00:00
}
2022-04-14 15:29:40 +00:00
2023-08-27 19:46:44 +00:00
_ , accessURLPortRaw , _ := net . SplitHostPort ( vals . AccessURL . Host )
2022-09-01 01:09:44 +00:00
if accessURLPortRaw == "" {
accessURLPortRaw = "80"
2023-08-27 19:46:44 +00:00
if vals . AccessURL . Scheme == "https" {
2022-09-01 01:09:44 +00:00
accessURLPortRaw = "443"
}
}
2023-03-07 21:10:01 +00:00
2022-09-01 01:09:44 +00:00
accessURLPort , err := strconv . Atoi ( accessURLPortRaw )
if err != nil {
return xerrors . Errorf ( "parse access URL port: %w" , err )
}
2022-07-31 22:49:25 +00:00
2023-08-27 19:46:44 +00:00
// Warn the user if the access URL is loopback or unresolvable.
isLocal , err := IsLocalURL ( ctx , vals . AccessURL . Value ( ) )
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
}
2023-03-23 22:42:20 +00:00
cliui . Warnf (
inv . Stderr ,
"The access URL %s %s, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\n" ,
2023-09-07 21:28:22 +00:00
pretty . Sprint ( cliui . DefaultStyles . Field , vals . AccessURL . String ( ) ) , reason ,
2023-03-07 21:10:01 +00:00
)
2022-03-22 19:17:50 +00:00
}
2022-03-24 15:07:33 +00:00
2022-10-07 13:05:56 +00:00
// A newline is added before for visibility in terminal output.
2023-08-27 19:46:44 +00:00
cliui . Infof ( inv . Stdout , "\nView the Web UI: %s" , vals . AccessURL . 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
}
2023-08-27 19:46:44 +00:00
sshKeygenAlgorithm , err := gitsshkey . ParseAlgorithm ( vals . SSHKeygenAlgorithm . String ( ) )
2022-04-06 00:18:26 +00:00
if err != nil {
2023-08-27 19:46:44 +00:00
return xerrors . Errorf ( "parse ssh keygen algorithm %s: %w" , vals . SSHKeygenAlgorithm , err )
2022-04-06 00:18:26 +00:00
}
2022-09-02 23:47:25 +00:00
defaultRegion := & tailcfg . DERPRegion {
2022-09-26 17:56:04 +00:00
EmbeddedRelay : true ,
2023-08-27 19:46:44 +00:00
RegionID : int ( vals . DERP . Server . RegionID . Value ( ) ) ,
RegionCode : vals . DERP . Server . RegionCode . String ( ) ,
RegionName : vals . DERP . Server . RegionName . String ( ) ,
2022-09-01 01:09:44 +00:00
Nodes : [ ] * tailcfg . DERPNode { {
2023-08-27 19:46:44 +00:00
Name : fmt . Sprintf ( "%db" , vals . DERP . Server . RegionID ) ,
RegionID : int ( vals . DERP . Server . RegionID . Value ( ) ) ,
HostName : vals . AccessURL . Value ( ) . Hostname ( ) ,
2022-09-01 01:09:44 +00:00
DERPPort : accessURLPort ,
STUNPort : - 1 ,
2023-08-27 19:46:44 +00:00
ForceHTTP : vals . AccessURL . Scheme == "http" ,
2022-09-01 01:09:44 +00:00
} } ,
2022-09-02 23:47:25 +00:00
}
2023-08-27 19:46:44 +00:00
if ! vals . DERP . Server . Enable {
2022-09-02 23:47:25 +00:00
defaultRegion = nil
}
2023-04-17 17:20:26 +00:00
2023-03-07 21:10:01 +00:00
derpMap , err := tailnet . NewDERPMap (
2023-08-27 19:46:44 +00:00
ctx , defaultRegion , vals . DERP . Server . STUNAddresses ,
vals . DERP . Config . URL . String ( ) , vals . DERP . Config . Path . String ( ) ,
vals . DERP . Config . BlockDirect . Value ( ) ,
2023-03-07 21:10:01 +00:00
)
2022-09-01 01:09:44 +00:00
if err != nil {
return xerrors . Errorf ( "create derp map: %w" , err )
}
2023-08-27 19:46:44 +00:00
appHostname := vals . WildcardAccessURL . String ( )
2022-10-14 18:25:11 +00:00
var appHostnameRegex * regexp . Regexp
if appHostname != "" {
2024-01-17 16:41:42 +00:00
appHostnameRegex , err = appurl . CompileHostnamePattern ( appHostname )
2022-10-14 18:25:11 +00:00
if err != nil {
return xerrors . Errorf ( "parse wildcard access URL %q: %w" , appHostname , err )
}
}
2022-09-22 22:30:32 +00:00
2023-10-03 14:04:39 +00:00
extAuthEnv , err := ReadExternalAuthProvidersFromEnv ( os . Environ ( ) )
2023-03-07 21:10:01 +00:00
if err != nil {
2023-10-03 14:04:39 +00:00
return xerrors . Errorf ( "read external auth providers from env: %w" , err )
2023-03-07 21:10:01 +00:00
}
2024-01-10 15:13:30 +00:00
promRegistry := prometheus . NewRegistry ( )
oauthInstrument := promoauth . NewFactory ( promRegistry )
2023-10-03 14:04:39 +00:00
vals . ExternalAuthConfigs . Value = append ( vals . ExternalAuthConfigs . Value , extAuthEnv ... )
2023-09-30 19:30:01 +00:00
externalAuthConfigs , err := externalauth . ConvertConfig (
2024-01-10 15:13:30 +00:00
oauthInstrument ,
2023-10-03 14:04:39 +00:00
vals . ExternalAuthConfigs . Value ,
2023-08-27 19:46:44 +00:00
vals . AccessURL . Value ( ) ,
2023-03-07 21:10:01 +00:00
)
2022-10-25 00:46:24 +00:00
if err != nil {
2023-09-30 19:30:01 +00:00
return xerrors . Errorf ( "convert external auth config: %w" , err )
2023-03-07 21:10:01 +00:00
}
2023-09-30 19:30:01 +00:00
for _ , c := range externalAuthConfigs {
2023-03-07 21:10:01 +00:00
logger . Debug (
2023-09-30 19:30:01 +00:00
ctx , "loaded external auth config" ,
2023-03-07 21:10:01 +00:00
slog . F ( "id" , c . ID ) ,
)
2022-10-25 00:46:24 +00:00
}
2023-08-27 19:46:44 +00:00
realIPConfig , err := httpmw . ParseRealIPConfig ( vals . ProxyTrustedHeaders , vals . ProxyTrustedOrigins )
2022-10-23 18:21:49 +00:00
if err != nil {
return xerrors . Errorf ( "parse real ip config: %w" , err )
}
2023-08-27 19:46:44 +00:00
configSSHOptions , err := vals . SSHConfig . ParseOptions ( )
2023-03-16 18:03:37 +00:00
if err != nil {
2023-08-27 19:46:44 +00:00
return xerrors . Errorf ( "parse ssh config options %q: %w" , vals . SSHConfig . SSHConfigOptions . String ( ) , err )
2023-03-16 18:03:37 +00:00
}
2022-03-24 15:07:33 +00:00
options := & coderd . Options {
2023-08-27 19:46:44 +00:00
AccessURL : vals . AccessURL . Value ( ) ,
2022-09-22 22:30:32 +00:00
AppHostname : appHostname ,
2022-10-14 18:25:11 +00:00
AppHostnameRegex : appHostnameRegex ,
2022-09-01 19:58:23 +00:00
Logger : logger . Named ( "coderd" ) ,
2023-10-30 17:42:20 +00:00
Database : dbmem . New ( ) ,
2023-07-26 16:21:04 +00:00
BaseDERPMap : derpMap ,
2023-06-14 15:34:54 +00:00
Pubsub : pubsub . NewInMemory ( ) ,
2022-12-06 17:05:14 +00:00
CacheDir : cacheDir ,
2022-09-01 19:58:23 +00:00
GoogleTokenValidator : googleTokenValidator ,
2023-09-30 19:30:01 +00:00
ExternalAuthConfigs : externalAuthConfigs ,
2022-10-23 18:21:49 +00:00
RealIPConfig : realIPConfig ,
2023-08-27 19:46:44 +00:00
SecureAuthCookie : vals . SecureAuthCookie . Value ( ) ,
2022-09-01 19:58:23 +00:00
SSHKeygenAlgorithm : sshKeygenAlgorithm ,
TracerProvider : tracerProvider ,
Telemetry : telemetry . NewNoop ( ) ,
2023-08-27 19:46:44 +00:00
MetricsCacheRefreshInterval : vals . MetricsCacheRefreshInterval . Value ( ) ,
AgentStatsRefreshInterval : vals . AgentStatRefreshInterval . Value ( ) ,
DeploymentValues : vals ,
2023-09-29 17:04:28 +00:00
// Do not pass secret values to DeploymentOptions. All values should be read from
// the DeploymentValues instead, this just serves to indicate the source of each
// option. This is just defensive to prevent accidentally leaking.
DeploymentOptions : codersdk . DeploymentOptionsWithoutSecrets ( opts ) ,
2024-01-10 15:13:30 +00:00
PrometheusRegistry : promRegistry ,
2023-08-27 19:46:44 +00:00
APIRateLimit : int ( vals . RateLimit . API . Value ( ) ) ,
2023-01-05 18:05:20 +00:00
LoginRateLimit : loginRateLimit ,
FilesRateLimit : filesRateLimit ,
2022-12-14 14:44:29 +00:00
HTTPClient : httpClient ,
2023-04-04 12:48:35 +00:00
TemplateScheduleStore : & atomic . Pointer [ schedule . TemplateScheduleStore ] { } ,
2023-07-20 13:35:41 +00:00
UserQuietHoursScheduleStore : & atomic . Pointer [ schedule . UserQuietHoursScheduleStore ] { } ,
2023-03-16 18:03:37 +00:00
SSHConfig : codersdk . SSHConfigResponse {
2023-08-27 19:46:44 +00:00
HostnamePrefix : vals . SSHConfig . DeploymentName . String ( ) ,
2023-03-16 18:03:37 +00:00
SSHConfigOptions : configSSHOptions ,
} ,
2023-12-15 18:38:47 +00:00
AllowWorkspaceRenames : vals . AllowWorkspaceRenames . Value ( ) ,
2022-10-10 19:04:15 +00:00
}
2023-04-13 14:07:19 +00:00
if httpServers . TLSConfig != nil {
options . TLSCertificates = httpServers . TLSConfig . Certificates
2022-10-17 13:43:30 +00:00
}
2022-10-10 19:04:15 +00:00
2023-08-27 19:46:44 +00:00
if vals . StrictTransportSecurity > 0 {
2023-03-07 21:10:01 +00:00
options . StrictTransportSecurityCfg , err = httpmw . HSTSConfigOptions (
2023-08-27 19:46:44 +00:00
int ( vals . StrictTransportSecurity . Value ( ) ) , vals . StrictTransportSecurityOptions ,
2023-03-07 21:10:01 +00:00
)
2023-02-10 16:52:49 +00:00
if err != nil {
2023-08-27 19:46:44 +00:00
return xerrors . Errorf ( "coderd: setting hsts header failed (options: %v): %w" , vals . StrictTransportSecurityOptions , err )
2023-02-10 16:52:49 +00:00
}
}
2023-08-27 19:46:44 +00:00
if vals . UpdateCheck {
2022-12-01 17:43:28 +00:00
options . UpdateCheckOptions = & updatecheck . Options {
// Avoid spamming GitHub API checking for updates.
Interval : 24 * time . Hour ,
// Inform server admins of new versions.
Notify : func ( r updatecheck . Result ) {
if semver . Compare ( r . Version , buildinfo . Version ( ) ) > 0 {
options . Logger . Info (
context . Background ( ) ,
"new version of coder available" ,
slog . F ( "new_version" , r . Version ) ,
slog . F ( "url" , r . URL ) ,
slog . F ( "upgrade_instructions" , "https://coder.com/docs/coder-oss/latest/admin/upgrade" ) ,
)
}
} ,
}
}
2023-08-27 19:46:44 +00:00
if vals . OAuth2 . Github . ClientSecret != "" {
2024-01-10 15:13:30 +00:00
options . GithubOAuth2Config , err = configureGithubOAuth2 (
oauthInstrument ,
vals . AccessURL . Value ( ) ,
2023-08-27 19:46:44 +00:00
vals . OAuth2 . Github . ClientID . String ( ) ,
vals . OAuth2 . Github . ClientSecret . String ( ) ,
vals . OAuth2 . Github . AllowSignups . Value ( ) ,
vals . OAuth2 . Github . AllowEveryone . Value ( ) ,
vals . OAuth2 . Github . AllowedOrgs ,
vals . OAuth2 . Github . AllowedTeams ,
vals . OAuth2 . Github . EnterpriseBaseURL . String ( ) ,
2022-10-10 19:04:15 +00:00
)
2022-04-23 22:58:57 +00:00
if err != nil {
return xerrors . Errorf ( "configure github oauth2: %w" , err )
}
}
2023-08-27 19:46:44 +00:00
if vals . OIDC . ClientKeyFile != "" || vals . OIDC . ClientSecret != "" {
if vals . OIDC . IgnoreEmailVerified {
2022-11-25 10:10:09 +00:00
logger . Warn ( ctx , "coder will not check email_verified for OIDC logins" )
}
2024-01-10 15:13:30 +00:00
// This OIDC config is **not** being instrumented with the
// oauth2 instrument wrapper. If we implement the missing
// oidc methods, then we can instrument it.
// Missing:
// - Userinfo
// - Verify
2023-08-27 19:46:44 +00:00
oc , err := createOIDCConfig ( ctx , vals )
2022-08-01 04:05:35 +00:00
if err != nil {
2023-08-27 19:46:44 +00:00
return xerrors . Errorf ( "create oidc config: %w" , err )
2022-08-01 04:05:35 +00:00
}
2023-08-27 19:46:44 +00:00
options . OIDCConfig = oc
2022-08-01 04:05:35 +00:00
}
2024-02-06 12:58:45 +00:00
// We'll read from this channel in the select below that tracks shutdown. If it remains
// nil, that case of the select will just never fire, but it's important not to have a
// "bare" read on this channel.
var pubsubWatchdogTimeout <- chan struct { }
2023-08-27 19:46:44 +00:00
if vals . InMemoryDatabase {
2023-05-31 13:55:57 +00:00
// This is only used for testing.
2023-10-30 17:42:20 +00:00
options . Database = dbmem . New ( )
2023-06-14 15:34:54 +00:00
options . Pubsub = pubsub . NewInMemory ( )
2022-06-15 21:02:18 +00:00
} else {
2024-03-20 17:14:43 +00:00
sqlDB , dbURL , err := getPostgresDB ( ctx , logger , vals . PostgresURL . String ( ) , codersdk . PostgresAuth ( vals . PostgresAuth ) , sqlDriver )
2022-03-24 15:07:33 +00:00
if err != nil {
2023-02-06 14:58:21 +00:00
return xerrors . Errorf ( "connect to postgres: %w" , err )
2022-03-24 15:07:33 +00:00
}
2023-02-06 14:58:21 +00:00
defer func ( ) {
_ = sqlDB . Close ( )
} ( )
2022-11-09 16:25:25 +00:00
2023-06-15 11:34:16 +00:00
options . Database = database . New ( sqlDB )
2024-02-01 07:25:03 +00:00
ps , err := pubsub . New ( ctx , logger . Named ( "pubsub" ) , sqlDB , dbURL )
2022-03-24 15:07:33 +00:00
if err != nil {
return xerrors . Errorf ( "create pubsub: %w" , err )
}
2024-02-01 07:25:03 +00:00
options . Pubsub = ps
if options . DeploymentValues . Prometheus . Enable {
options . PrometheusRegistry . MustRegister ( ps )
}
2022-07-27 15:21:21 +00:00
defer options . Pubsub . Close ( )
2024-02-06 12:58:45 +00:00
psWatchdog := pubsub . NewWatchdog ( ctx , logger . Named ( "pswatch" ) , ps )
pubsubWatchdogTimeout = psWatchdog . Timeout ( )
defer psWatchdog . Close ( )
2022-03-24 15:07:33 +00:00
}
2023-06-15 11:34:16 +00:00
if options . DeploymentValues . Prometheus . Enable && options . DeploymentValues . Prometheus . CollectDBMetrics {
options . Database = dbmetrics . New ( options . Database , options . PrometheusRegistry )
}
2023-03-07 19:38:11 +00:00
var deploymentID string
err = options . Database . InTx ( func ( tx database . Store ) error {
// This will block until the lock is acquired, and will be
// automatically released when the transaction ends.
err := tx . AcquireLock ( ctx , database . LockIDDeploymentSetup )
2022-06-17 05:26:40 +00:00
if err != nil {
2023-03-07 19:38:11 +00:00
return xerrors . Errorf ( "acquire lock: %w" , err )
}
deploymentID , err = tx . GetDeploymentID ( ctx )
if err != nil && ! xerrors . Is ( err , sql . ErrNoRows ) {
return xerrors . Errorf ( "get deployment id: %w" , err )
}
if deploymentID == "" {
deploymentID = uuid . NewString ( )
err = tx . InsertDeploymentID ( ctx , deploymentID )
if err != nil {
return xerrors . Errorf ( "set deployment id: %w" , err )
}
}
2023-04-05 18:41:55 +00:00
// Read the app signing key from the DB. We store it hex encoded
// since the config table uses strings for the value and we
// don't want to deal with automatic encoding issues.
appSecurityKeyStr , err := tx . GetAppSecurityKey ( ctx )
2023-03-07 19:38:11 +00:00
if err != nil && ! xerrors . Is ( err , sql . ErrNoRows ) {
return xerrors . Errorf ( "get app signing key: %w" , err )
2022-06-17 05:26:40 +00:00
}
2023-04-05 18:41:55 +00:00
// If the string in the DB is an invalid hex string or the
// length is not equal to the current key length, generate a new
// one.
//
// If the key is regenerated, old signed tokens and encrypted
// strings will become invalid. New signed app tokens will be
// generated automatically on failure. Any workspace app token
// smuggling operations in progress may fail, although with a
// helpful error.
if decoded , err := hex . DecodeString ( appSecurityKeyStr ) ; err != nil || len ( decoded ) != len ( workspaceapps . SecurityKey { } ) {
b := make ( [ ] byte , len ( workspaceapps . SecurityKey { } ) )
2023-03-07 19:38:11 +00:00
_ , err := rand . Read ( b )
if err != nil {
return xerrors . Errorf ( "generate fresh app signing key: %w" , err )
}
2023-04-05 18:41:55 +00:00
appSecurityKeyStr = hex . EncodeToString ( b )
err = tx . UpsertAppSecurityKey ( ctx , appSecurityKeyStr )
2023-03-07 19:38:11 +00:00
if err != nil {
return xerrors . Errorf ( "insert freshly generated app signing key to database: %w" , err )
}
}
2023-04-05 18:41:55 +00:00
appSecurityKey , err := workspaceapps . KeyFromString ( appSecurityKeyStr )
2023-03-07 19:38:11 +00:00
if err != nil {
2023-04-05 18:41:55 +00:00
return xerrors . Errorf ( "decode app signing key from database: %w" , err )
2023-03-07 19:38:11 +00:00
}
2023-04-05 18:41:55 +00:00
options . AppSecurityKey = appSecurityKey
2023-06-30 12:38:48 +00:00
// Read the oauth signing key from the database. Like the app security, generate a new one
// if it is invalid for any reason.
oauthSigningKeyStr , err := tx . GetOAuthSigningKey ( ctx )
if err != nil && ! xerrors . Is ( err , sql . ErrNoRows ) {
return xerrors . Errorf ( "get app oauth signing key: %w" , err )
}
if decoded , err := hex . DecodeString ( oauthSigningKeyStr ) ; err != nil || len ( decoded ) != len ( options . OAuthSigningKey ) {
b := make ( [ ] byte , len ( options . OAuthSigningKey ) )
_ , err := rand . Read ( b )
if err != nil {
return xerrors . Errorf ( "generate fresh oauth signing key: %w" , err )
}
oauthSigningKeyStr = hex . EncodeToString ( b )
err = tx . UpsertOAuthSigningKey ( ctx , oauthSigningKeyStr )
if err != nil {
return xerrors . Errorf ( "insert freshly generated oauth signing key to database: %w" , err )
}
}
keyBytes , err := hex . DecodeString ( oauthSigningKeyStr )
if err != nil {
return xerrors . Errorf ( "decode oauth signing key from database: %w" , err )
}
if len ( keyBytes ) != len ( options . OAuthSigningKey ) {
return xerrors . Errorf ( "oauth signing key in database is not the correct length, expect %d got %d" , len ( options . OAuthSigningKey ) , len ( keyBytes ) )
}
copy ( options . OAuthSigningKey [ : ] , keyBytes )
if options . OAuthSigningKey == [ 32 ] byte { } {
return xerrors . Errorf ( "oauth signing key in database is empty" )
}
2023-03-07 19:38:11 +00:00
return nil
} , nil )
if err != nil {
return err
2022-06-17 05:26:40 +00:00
}
2024-04-01 20:02:50 +00:00
// This should be output before the logs start streaming.
cliui . Infof ( inv . Stdout , "\n==> Logs will stream in below (press ctrl+c to gracefully exit):" )
2023-08-27 19:46:44 +00:00
if vals . Telemetry . Enable {
2022-11-14 16:11:08 +00:00
gitAuth := make ( [ ] telemetry . GitAuth , 0 )
2023-03-07 21:10:01 +00:00
// TODO:
2023-10-03 14:04:39 +00:00
var gitAuthConfigs [ ] codersdk . ExternalAuthConfig
2022-11-14 16:11:08 +00:00
for _ , cfg := range gitAuthConfigs {
gitAuth = append ( gitAuth , telemetry . GitAuth {
2023-03-07 21:10:01 +00:00
Type : cfg . Type ,
2022-11-14 16:11:08 +00:00
} )
}
2022-06-17 05:26:40 +00:00
options . Telemetry , err = telemetry . New ( telemetry . Options {
2022-11-14 16:11:08 +00:00
BuiltinPostgres : builtinPostgres ,
DeploymentID : deploymentID ,
Database : options . Database ,
Logger : logger . Named ( "telemetry" ) ,
2023-08-27 19:46:44 +00:00
URL : vals . Telemetry . URL . Value ( ) ,
Wildcard : vals . WildcardAccessURL . String ( ) != "" ,
DERPServerRelayURL : vals . DERP . Server . RelayURL . String ( ) ,
2022-11-14 16:11:08 +00:00
GitAuth : gitAuth ,
2023-08-27 19:46:44 +00:00
GitHubOAuth : vals . OAuth2 . Github . ClientID != "" ,
OIDCAuth : vals . OIDC . ClientID != "" ,
OIDCIssuerURL : vals . OIDC . IssuerURL . String ( ) ,
Prometheus : vals . Prometheus . Enable . Value ( ) ,
STUN : len ( vals . DERP . Server . STUNAddresses ) != 0 ,
2022-11-14 16:11:08 +00:00
Tunnel : tunnel != nil ,
2024-03-19 09:05:29 +00:00
Experiments : vals . Experiments . Value ( ) ,
2023-12-14 21:52:52 +00:00
ParseLicenseJWT : func ( lic * telemetry . License ) error {
// This will be nil when running in AGPL-only mode.
if options . ParseLicenseClaims == nil {
return nil
}
email , trial , err := options . ParseLicenseClaims ( lic . JWT )
if err != nil {
return err
}
if email != "" {
lic . Email = & email
}
lic . Trial = & trial
return nil
} ,
2022-06-17 05:26:40 +00:00
} )
if err != nil {
return xerrors . Errorf ( "create telemetry reporter: %w" , err )
}
defer options . Telemetry . Close ( )
2023-10-19 20:50:20 +00:00
} else {
logger . Warn ( ctx , ` telemetry disabled, unable to notify of security issues. Read more: https://coder.com/docs/v2/latest/admin/telemetry ` )
2022-06-17 05:26:40 +00:00
}
2022-08-08 15:09:46 +00:00
// This prevents the pprof import from being accidentally deleted.
_ = pprof . Handler
2023-08-27 19:46:44 +00:00
if vals . Pprof . Enable {
2022-08-08 15:09:46 +00:00
//nolint:revive
2023-08-27 19:46:44 +00:00
defer ServeHandler ( ctx , logger , nil , vals . Pprof . Address . String ( ) , "pprof" ) ( )
2022-08-08 15:09:46 +00:00
}
2023-08-27 19:46:44 +00:00
if vals . Prometheus . Enable {
closeFn , err := enablePrometheus (
ctx ,
logger . Named ( "prometheus" ) ,
vals ,
options ,
)
2022-08-09 01:08:42 +00:00
if err != nil {
2023-08-27 19:46:44 +00:00
return xerrors . Errorf ( "enable prometheus: %w" , err )
2023-04-14 14:14:52 +00:00
}
2023-08-27 19:46:44 +00:00
defer closeFn ( )
2022-08-08 15:09:46 +00:00
}
2023-08-27 19:46:44 +00:00
if vals . Swagger . Enable {
options . SwaggerEndpoint = vals . Swagger . Enable . Value ( )
2022-12-19 17:43:46 +00:00
}
2023-08-04 16:00:42 +00:00
batcher , closeBatcher , err := batchstats . New ( ctx ,
batchstats . WithLogger ( options . Logger . Named ( "batchstats" ) ) ,
batchstats . WithStore ( options . Database ) ,
)
if err != nil {
return xerrors . Errorf ( "failed to create agent stats batcher: %w" , err )
}
options . StatsBatcher = batcher
defer closeBatcher ( )
2022-10-17 23:36:23 +00:00
// We use a separate coderAPICloser so the Enterprise API
2023-12-15 07:38:12 +00:00
// can have its own close functions. This is cleaner
2022-10-17 13:43:30 +00:00
// than abstracting the Coder API itself.
2022-10-17 23:36:23 +00:00
coderAPI , coderAPICloser , err := newAPI ( ctx , options )
2022-09-20 04:11:01 +00:00
if err != nil {
2023-03-23 22:42:20 +00:00
return xerrors . Errorf ( "create coder API: %w" , err )
2022-09-20 04:11:01 +00:00
}
2022-07-27 15:21:21 +00:00
2023-08-27 19:46:44 +00:00
if vals . Prometheus . Enable {
2023-04-07 15:48:52 +00:00
// Agent metrics require reference to the tailnet coordinator, so must be initiated after Coder API.
2023-07-26 16:21:04 +00:00
closeAgentsFunc , err := prometheusmetrics . Agents ( ctx , logger , options . PrometheusRegistry , coderAPI . Database , & coderAPI . TailnetCoordinator , coderAPI . DERPMap , coderAPI . Options . AgentInactiveDisconnectTimeout , 0 )
2023-04-07 15:48:52 +00:00
if err != nil {
return xerrors . Errorf ( "register agents prometheus metric: %w" , err )
}
defer closeAgentsFunc ( )
2024-03-19 12:11:27 +00:00
var active codersdk . Experiments
for _ , exp := range options . DeploymentValues . Experiments . Value ( ) {
active = append ( active , codersdk . Experiment ( exp ) )
}
if err = prometheusmetrics . Experiments ( options . PrometheusRegistry , active ) ; err != nil {
return xerrors . Errorf ( "register experiments metric: %w" , err )
}
2023-04-07 15:48:52 +00:00
}
2022-03-22 19:17:50 +00:00
client := codersdk . New ( localURL )
2023-04-13 14:07:19 +00:00
if localURL . Scheme == "https" && IsLocalhost ( localURL . Hostname ( ) ) {
2022-12-19 19:25:59 +00:00
// The certificate will likely be self-signed or for a different
// hostname, so we need to skip verification.
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-12-19 19:25:59 +00:00
defer client . HTTPClient . CloseIdleConnections ( )
2022-03-22 19:17:50 +00:00
2022-12-15 20:09:19 +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.
2023-03-07 21:10:01 +00:00
err = config . URL ( ) . Write ( client . URL . String ( ) )
if err != nil && flag . Lookup ( "test.v" ) != nil {
return xerrors . Errorf ( "write config url: %w" , err )
}
2022-12-15 20:09:19 +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 ( )
}
} ( )
2023-03-23 22:42:20 +00:00
var provisionerdWaitGroup sync . WaitGroup
defer provisionerdWaitGroup . Wait ( )
2022-11-05 00:03:01 +00:00
provisionerdMetrics := provisionerd . NewMetrics ( options . PrometheusRegistry )
2024-05-03 15:14:26 +00:00
// Built in provisioner daemons will support the same types.
// By default, this is the slice {"terraform"}
provisionerTypes := make ( [ ] codersdk . ProvisionerType , 0 )
for _ , pt := range vals . Provisioner . DaemonTypes {
provisionerTypes = append ( provisionerTypes , codersdk . ProvisionerType ( pt ) )
}
2023-08-27 19:46:44 +00:00
for i := int64 ( 0 ) ; i < vals . Provisioner . Daemons . Value ( ) ; i ++ {
2023-12-07 16:59:13 +00:00
suffix := fmt . Sprintf ( "%d" , i )
// The suffix is added to the hostname, so we may need to trim to fit into
// the 64 character limit.
hostname := stringutil . Truncate ( cliutil . Hostname ( ) , 63 - len ( suffix ) )
name := fmt . Sprintf ( "%s-%s" , hostname , suffix )
2022-12-06 17:05:14 +00:00
daemonCacheDir := filepath . Join ( cacheDir , fmt . Sprintf ( "provisioner-%d" , i ) )
2023-03-23 22:42:20 +00:00
daemon , err := newProvisionerDaemon (
2024-05-03 15:14:26 +00:00
ctx , coderAPI , provisionerdMetrics , logger , vals , daemonCacheDir , errCh , & provisionerdWaitGroup , name , provisionerTypes ,
2023-03-23 22:42:20 +00:00
)
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 )
}
2023-06-06 21:50:11 +00:00
provisionerdMetrics . Runner . NumDaemons . Set ( float64 ( len ( provisionerDaemons ) ) )
2022-07-27 15:21:21 +00:00
shutdownConnsCtx , shutdownConns := context . WithCancel ( ctx )
defer shutdownConns ( )
2022-08-29 17:07:49 +00:00
2023-03-23 19:09:13 +00:00
// Ensures that old database entries are cleaned up over time!
2024-04-22 10:11:50 +00:00
purger := dbpurge . New ( ctx , logger . Named ( "dbpurge" ) , options . Database )
2023-03-23 19:09:13 +00:00
defer purger . Close ( )
2024-03-20 16:44:12 +00:00
// Updates workspace usage
tracker := workspaceusage . New ( options . Database ,
workspaceusage . WithLogger ( logger . Named ( "workspace_usage_tracker" ) ) ,
)
options . WorkspaceUsageTracker = tracker
defer tracker . Close ( )
2022-12-15 20:09:19 +00:00
// Wrap the server in middleware that redirects to the access URL if
// the request is not to a local IP.
var handler http . Handler = coderAPI . RootHandler
2023-08-27 19:46:44 +00:00
if vals . RedirectToAccessURL {
handler = redirectToAccessURL ( handler , vals . AccessURL . Value ( ) , tunnel != nil , appHostnameRegex )
2022-12-15 20:09:19 +00:00
}
// ReadHeaderTimeout is purposefully not enabled. It caused some
// issues with websockets over the dev tunnel.
2022-08-29 17:07:49 +00:00
// See: https://github.com/coder/coder/pull/3730
//nolint:gosec
2022-12-15 20:09:19 +00:00
httpServer := & http . Server {
// These errors are typically noise like "TLS: EOF". Vault does
// similar:
2022-07-27 15:21:21 +00:00
// https://github.com/hashicorp/vault/blob/e2490059d0711635e529a4efcbaa1b26998d6e1c/command/server.go#L2714
2022-08-29 17:07:49 +00:00
ErrorLog : log . New ( io . Discard , "" , 0 ) ,
2022-12-15 20:09:19 +00:00
Handler : handler ,
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-12-15 20:09:19 +00:00
_ = shutdownWithTimeout ( httpServer . Shutdown , 5 * time . Second )
2022-03-24 15:07:33 +00:00
} ( )
2022-03-22 19:17:50 +00:00
2022-12-15 20:09:19 +00:00
// We call this in the routine so we can kill the other listeners if
// one of them fails.
closeListenersNow := func ( ) {
2023-04-13 14:07:19 +00:00
httpServers . Close ( )
2022-10-07 13:05:56 +00:00
if tunnel != nil {
2022-12-15 20:09:19 +00:00
_ = tunnel . Listener . Close ( )
2022-03-24 19:21:05 +00:00
}
2022-12-15 20:09:19 +00:00
}
2022-06-10 18:38:11 +00:00
2022-12-15 20:09:19 +00:00
eg := errgroup . Group { }
2023-04-13 14:07:19 +00:00
eg . Go ( func ( ) error {
defer closeListenersNow ( )
return httpServers . Serve ( httpServer )
} )
2022-10-07 13:05:56 +00:00
if tunnel != nil {
2022-07-27 15:21:21 +00:00
eg . Go ( func ( ) error {
2022-12-15 20:09:19 +00:00
defer closeListenersNow ( )
return httpServer . Serve ( tunnel . Listener )
2022-06-10 18:38:11 +00:00
} )
2022-07-27 15:21:21 +00:00
}
2022-12-15 20:09:19 +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-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 )
}
2023-08-27 19:46:44 +00:00
autobuildTicker := time . NewTicker ( vals . AutobuildPollInterval . Value ( ) )
2023-06-25 13:17:00 +00:00
defer autobuildTicker . Stop ( )
2023-09-19 06:25:57 +00:00
autobuildExecutor := autobuild . NewExecutor (
2023-10-18 22:07:21 +00:00
ctx , options . Database , options . Pubsub , coderAPI . TemplateScheduleStore , & coderAPI . Auditor , coderAPI . AccessControlStore , logger , autobuildTicker . C )
2022-05-20 10:57:02 +00:00
autobuildExecutor . Run ( )
2022-05-11 22:03:02 +00:00
2023-08-27 19:46:44 +00:00
hangDetectorTicker := time . NewTicker ( vals . JobHangDetectorInterval . Value ( ) )
2023-06-25 13:17:00 +00:00
defer hangDetectorTicker . Stop ( )
hangDetector := unhanger . New ( ctx , options . Database , options . Pubsub , logger , hangDetectorTicker . C )
hangDetector . Start ( )
defer hangDetector . Close ( )
2024-03-15 13:16:36 +00:00
waitForProvisionerJobs := false
2022-07-27 15:21:21 +00:00
// 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 {
2024-03-15 13:16:36 +00:00
case <- stopCtx . Done ( ) :
exitErr = stopCtx . Err ( )
waitForProvisionerJobs = true
_ , _ = io . WriteString ( inv . Stdout , cliui . Bold ( "Stop caught, waiting for provisioner jobs to complete and gracefully exiting. Use ctrl+\\ to force quit" ) )
case <- interruptCtx . Done ( ) :
exitErr = interruptCtx . Err ( )
2023-09-07 21:28:22 +00:00
_ , _ = io . WriteString ( inv . Stdout , cliui . Bold ( "Interrupt caught, gracefully exiting. Use ctrl+\\ to force quit" ) )
2023-03-22 13:13:48 +00:00
case <- tunnelDone :
exitErr = xerrors . New ( "dev tunnel closed unexpectedly" )
2024-02-06 12:58:45 +00:00
case <- pubsubWatchdogTimeout :
exitErr = xerrors . New ( "pubsub Watchdog timed out" )
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 ) {
2023-03-23 22:42:20 +00:00
cliui . Errorf ( inv . Stderr , "Unexpected error, shutting down server: %s\n" , exitErr )
2022-07-27 15:21:21 +00:00
}
// 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 {
2023-03-23 22:42:20 +00:00
cliui . Errorf ( inv . Stderr , "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.
2023-03-23 22:42:20 +00:00
cliui . Info ( inv . Stdout , "Shutting down API server..." + "\n" )
2022-12-15 20:09:19 +00:00
err = shutdownWithTimeout ( httpServer . Shutdown , 3 * time . Second )
2022-07-27 15:21:21 +00:00
if err != nil {
2023-03-23 22:42:20 +00:00
cliui . Errorf ( inv . Stderr , "API server shutdown took longer than 3s: %s\n" , err )
2022-07-27 15:21:21 +00:00
} else {
2023-03-23 22:42:20 +00:00
cliui . Info ( inv . Stdout , "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 ( )
2023-08-27 19:46:44 +00:00
r . Verbosef ( inv , "Shutting down provisioner daemon %d..." , id )
2024-03-15 13:16:36 +00:00
timeout := 5 * time . Second
if waitForProvisionerJobs {
// It can last for a long time...
timeout = 30 * time . Minute
}
err := shutdownWithTimeout ( func ( ctx context . Context ) error {
// We only want to cancel active jobs if we aren't exiting gracefully.
return provisionerDaemon . Shutdown ( ctx , ! waitForProvisionerJobs )
} , timeout )
2022-07-27 15:21:21 +00:00
if err != nil {
2023-11-07 14:55:39 +00:00
cliui . Errorf ( inv . Stderr , "Failed to shut down provisioner daemon %d: %s\n" , id , err )
2022-07-27 15:21:21 +00:00
return
}
err = provisionerDaemon . Close ( )
if err != nil {
2023-03-23 22:42:20 +00:00
cliui . Errorf ( inv . Stderr , "Close provisioner daemon %d: %s\n" , id , err )
2022-07-27 15:21:21 +00:00
return
}
2023-08-27 19:46:44 +00:00
r . Verbosef ( inv , "Gracefully shut down provisioner daemon %d" , id )
2022-07-27 15:21:21 +00:00
} ( )
}
wg . Wait ( )
2023-03-23 22:42:20 +00:00
cliui . Info ( inv . Stdout , "Waiting for WebSocket connections to close..." + "\n" )
2022-10-17 23:36:23 +00:00
_ = coderAPICloser . Close ( )
2023-03-23 22:42:20 +00:00
cliui . Info ( inv . Stdout , "Done waiting for WebSocket connections" + "\n" )
2022-07-27 15:21:21 +00:00
// Close tunnel after we no longer have in-flight connections.
2022-10-07 13:05:56 +00:00
if tunnel != nil {
2023-03-23 22:42:20 +00:00
cliui . Infof ( inv . Stdout , "Waiting for tunnel to close..." )
2023-03-22 13:13:48 +00:00
_ = tunnel . Close ( )
<- tunnel . Wait ( )
2023-03-23 22:42:20 +00:00
cliui . Infof ( inv . Stdout , "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 ( )
2023-03-23 22:42:20 +00:00
switch {
case xerrors . Is ( exitErr , context . DeadlineExceeded ) :
cliui . Warnf ( inv . Stderr , "Graceful shutdown timed out" )
// Errors here cause a significant number of benign CI failures.
return nil
case xerrors . Is ( exitErr , context . Canceled ) :
return nil
case exitErr != nil :
return xerrors . Errorf ( "graceful shutdown: %w" , exitErr )
default :
2022-09-20 04:56:51 +00:00
return nil
}
2022-03-22 19:17:50 +00:00
} ,
}
2022-03-28 19:26:41 +00:00
2022-12-20 18:51:17 +00:00
var pgRawURL bool
2023-03-23 22:42:20 +00:00
2024-03-17 14:45:26 +00:00
postgresBuiltinURLCmd := & serpent . Command {
2022-06-15 21:02:18 +00:00
Use : "postgres-builtin-url" ,
Short : "Output the connection URL for the built-in PostgreSQL deployment." ,
2024-03-15 16:24:38 +00:00
Handler : func ( inv * serpent . Invocation ) error {
2023-03-23 22:42:20 +00:00
url , err := embeddedPostgresURL ( r . createConfig ( ) )
2022-06-15 21:02:18 +00:00
if err != nil {
return err
}
2022-12-20 18:51:17 +00:00
if pgRawURL {
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintf ( inv . Stdout , "%s\n" , url )
2022-12-20 18:51:17 +00:00
} else {
2023-09-07 21:28:22 +00:00
_ , _ = fmt . Fprintf ( inv . Stdout , "%s\n" , pretty . Sprint ( cliui . DefaultStyles . Code , fmt . Sprintf ( "psql %q" , url ) ) )
2022-12-20 18:51:17 +00:00
}
2022-06-15 21:02:18 +00:00
return nil
} ,
2022-12-20 18:51:17 +00:00
}
2023-03-23 22:42:20 +00:00
2024-03-17 14:45:26 +00:00
postgresBuiltinServeCmd := & serpent . Command {
2022-07-14 21:51:44 +00:00
Use : "postgres-builtin-serve" ,
Short : "Run the built-in PostgreSQL deployment." ,
2024-03-15 16:24:38 +00:00
Handler : func ( inv * serpent . Invocation ) error {
2023-03-23 22:42:20 +00:00
ctx := inv . Context ( )
2022-12-20 18:51:17 +00:00
2023-03-23 22:42:20 +00:00
cfg := r . createConfig ( )
2023-11-14 18:56:27 +00:00
logger := inv . Logger . AppendSinks ( sloghuman . Sink ( inv . Stderr ) )
2023-03-23 22:42:20 +00:00
if ok , _ := inv . ParsedFlags ( ) . GetBool ( varVerbose ) ; ok {
2022-07-14 21:51:44 +00:00
logger = logger . Leveled ( slog . LevelDebug )
}
2023-11-13 11:14:42 +00:00
ctx , cancel := inv . SignalNotifyContext ( ctx , InterruptSignals ... )
2022-12-20 18:51:17 +00:00
defer cancel ( )
url , closePg , err := startBuiltinPostgres ( ctx , cfg , logger )
2022-07-14 21:51:44 +00:00
if err != nil {
return err
}
defer func ( ) { _ = closePg ( ) } ( )
2022-12-20 18:51:17 +00:00
if pgRawURL {
2023-03-23 22:42:20 +00:00
_ , _ = fmt . Fprintf ( inv . Stdout , "%s\n" , url )
2022-12-20 18:51:17 +00:00
} else {
2023-09-07 21:28:22 +00:00
_ , _ = fmt . Fprintf ( inv . Stdout , "%s\n" , pretty . Sprint ( cliui . DefaultStyles . Code , fmt . Sprintf ( "psql %q" , url ) ) )
2022-12-20 18:51:17 +00:00
}
2022-07-14 21:51:44 +00:00
2022-12-20 18:51:17 +00:00
<- ctx . Done ( )
2022-07-14 21:51:44 +00:00
return nil
} ,
2022-12-20 18:51:17 +00:00
}
2023-03-23 22:42:20 +00:00
createAdminUserCmd := r . newCreateAdminUserCommand ( )
2024-03-15 16:24:38 +00:00
rawURLOpt := serpent . Option {
2023-03-23 22:42:20 +00:00
Flag : "raw-url" ,
2024-03-15 16:24:38 +00:00
Value : serpent . BoolOf ( & pgRawURL ) ,
2023-03-23 22:42:20 +00:00
Description : "Output the raw connection URL instead of a psql command." ,
}
createAdminUserCmd . Options . Add ( rawURLOpt )
postgresBuiltinURLCmd . Options . Add ( rawURLOpt )
postgresBuiltinServeCmd . Options . Add ( rawURLOpt )
serverCmd . Children = append (
serverCmd . Children ,
createAdminUserCmd , postgresBuiltinURLCmd , postgresBuiltinServeCmd ,
)
2022-07-14 21:51:44 +00:00
2023-03-23 22:42:20 +00:00
return serverCmd
2022-03-22 19:17:50 +00:00
}
2023-04-11 19:47:07 +00:00
// printDeprecatedOptions loops through all command options, and prints
// a warning for usage of deprecated options.
2024-03-15 16:24:38 +00:00
func PrintDeprecatedOptions ( ) serpent . MiddlewareFunc {
return func ( next serpent . HandlerFunc ) serpent . HandlerFunc {
return func ( inv * serpent . Invocation ) error {
2023-04-11 19:47:07 +00:00
opts := inv . Command . Options
// Print deprecation warnings.
for _ , opt := range opts {
if opt . UseInstead == nil {
continue
}
2024-03-15 16:24:38 +00:00
if opt . ValueSource == serpent . ValueSourceNone || opt . ValueSource == serpent . ValueSourceDefault {
2023-04-11 19:47:07 +00:00
continue
}
warnStr := opt . Name + " is deprecated, please use "
for i , use := range opt . UseInstead {
warnStr += use . Name + " "
if i != len ( opt . UseInstead ) - 1 {
warnStr += "and "
}
}
warnStr += "instead.\n"
cliui . Warn ( inv . Stderr ,
warnStr ,
)
}
return next ( inv )
}
}
}
// writeConfigMW will prevent the main command from running if the write-config
// flag is set. Instead, it will marshal the command options to YAML and write
// them to stdout.
2024-03-15 16:24:38 +00:00
func WriteConfigMW ( cfg * codersdk . DeploymentValues ) serpent . MiddlewareFunc {
return func ( next serpent . HandlerFunc ) serpent . HandlerFunc {
return func ( inv * serpent . Invocation ) error {
2023-04-11 19:47:07 +00:00
if ! cfg . WriteConfig {
return next ( inv )
}
opts := inv . Command . Options
n , err := opts . MarshalYAML ( )
if err != nil {
return xerrors . Errorf ( "generate yaml: %w" , err )
}
enc := yaml . NewEncoder ( inv . Stdout )
enc . SetIndent ( 2 )
err = enc . Encode ( n )
if err != nil {
return xerrors . Errorf ( "encode yaml: %w" , err )
}
err = enc . Close ( )
if err != nil {
return xerrors . Errorf ( "close yaml encoder: %w" , err )
}
return nil
}
}
}
2022-07-31 22:49:25 +00:00
// isLocalURL returns true if the hostname of the provided URL appears to
// resolve to a loopback address.
2023-04-13 14:07:19 +00:00
func IsLocalURL ( ctx context . Context , u * url . URL ) ( bool , error ) {
2023-10-24 19:12:03 +00:00
// In tests, we commonly use "example.com" or "google.com", which
// are not loopback, so avoid the DNS lookup to avoid flakes.
if flag . Lookup ( "test.v" ) != nil {
if u . Hostname ( ) == "example.com" || u . Hostname ( ) == "google.com" {
return false , nil
}
}
2022-07-31 22:49:25 +00:00
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-09-20 04:35:18 +00:00
func shutdownWithTimeout ( shutdown func ( context . Context ) error , timeout time . Duration ) error {
2022-07-27 15:21:21 +00:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , timeout )
defer cancel ( )
2022-09-20 04:35:18 +00:00
return shutdown ( ctx )
2022-07-27 15:21:21 +00:00
}
2022-05-19 22:47:45 +00:00
// nolint:revive
2022-09-16 16:43:22 +00:00
func newProvisionerDaemon (
ctx context . Context ,
coderAPI * coderd . API ,
2022-11-05 00:03:01 +00:00
metrics provisionerd . Metrics ,
2022-09-16 16:43:22 +00:00
logger slog . Logger ,
2023-03-07 21:10:01 +00:00
cfg * codersdk . DeploymentValues ,
2022-12-06 17:05:14 +00:00
cacheDir string ,
2022-09-16 16:43:22 +00:00
errCh chan error ,
2023-03-23 22:42:20 +00:00
wg * sync . WaitGroup ,
2023-12-07 16:59:13 +00:00
name string ,
2024-05-03 15:14:26 +00:00
provisionerTypes [ ] codersdk . ProvisionerType ,
2022-07-27 15:21:21 +00:00
) ( srv * provisionerd . Server , err error ) {
ctx , cancel := context . WithCancel ( ctx )
defer func ( ) {
if err != nil {
cancel ( )
}
} ( )
2022-12-06 17:05:14 +00:00
err = os . MkdirAll ( cacheDir , 0 o700 )
2022-03-30 22:59:54 +00:00
if err != nil {
2022-12-06 17:05:14 +00:00
return nil , xerrors . Errorf ( "mkdir %q: %w" , cacheDir , err )
2022-03-30 22:59:54 +00:00
}
2023-05-23 17:37:27 +00:00
workDir := filepath . Join ( cacheDir , "work" )
err = os . MkdirAll ( workDir , 0 o700 )
2022-03-22 19:17:50 +00:00
if err != nil {
2023-05-23 17:37:27 +00:00
return nil , xerrors . Errorf ( "mkdir work dir: %w" , err )
2022-03-22 19:17:50 +00:00
}
2022-03-29 19:59:32 +00:00
2024-05-03 15:14:26 +00:00
// Omit any duplicates
provisionerTypes = slice . Unique ( provisionerTypes )
// Populate the connector with the supported types.
2023-09-08 09:53:48 +00:00
connector := provisionerd . LocalProvisioners { }
2024-05-03 15:14:26 +00:00
for _ , provisionerType := range provisionerTypes {
switch provisionerType {
case codersdk . ProvisionerTypeEcho :
echoClient , echoServer := drpc . MemTransportPipe ( )
wg . Add ( 1 )
go func ( ) {
defer wg . Done ( )
<- ctx . Done ( )
_ = echoClient . Close ( )
_ = echoServer . Close ( )
} ( )
wg . Add ( 1 )
go func ( ) {
defer wg . Done ( )
defer cancel ( )
2022-07-27 15:21:21 +00:00
2024-05-03 15:14:26 +00:00
err := echo . Serve ( ctx , & provisionersdk . ServeOptions {
Listener : echoServer ,
WorkDirectory : workDir ,
Logger : logger . Named ( "echo" ) ,
} )
if err != nil {
select {
case errCh <- err :
default :
}
2022-07-27 15:21:21 +00:00
}
2024-05-03 15:14:26 +00:00
} ( )
connector [ string ( database . ProvisionerTypeEcho ) ] = sdkproto . NewDRPCProvisionerClient ( echoClient )
case codersdk . ProvisionerTypeTerraform :
tfDir := filepath . Join ( cacheDir , "tf" )
err = os . MkdirAll ( tfDir , 0 o700 )
if err != nil {
return nil , xerrors . Errorf ( "mkdir terraform dir: %w" , err )
2022-05-19 22:47:45 +00:00
}
2023-06-22 00:21:40 +00:00
2024-05-03 15:14:26 +00:00
tracer := coderAPI . TracerProvider . Tracer ( tracing . TracerName )
terraformClient , terraformServer := drpc . MemTransportPipe ( )
wg . Add ( 1 )
go func ( ) {
defer wg . Done ( )
<- ctx . Done ( )
_ = terraformClient . Close ( )
_ = terraformServer . Close ( )
} ( )
wg . Add ( 1 )
go func ( ) {
defer wg . Done ( )
defer cancel ( )
err := terraform . Serve ( ctx , & terraform . ServeOptions {
ServeOptions : & provisionersdk . ServeOptions {
Listener : terraformServer ,
Logger : logger . Named ( "terraform" ) ,
WorkDirectory : workDir ,
} ,
CachePath : tfDir ,
Tracer : tracer ,
} )
if err != nil && ! xerrors . Is ( err , context . Canceled ) {
select {
case errCh <- err :
default :
}
2023-06-22 00:21:40 +00:00
}
2024-05-03 15:14:26 +00:00
} ( )
2023-06-22 00:21:40 +00:00
2024-05-03 15:14:26 +00:00
connector [ string ( database . ProvisionerTypeTerraform ) ] = sdkproto . NewDRPCProvisionerClient ( terraformClient )
default :
2024-05-03 22:12:06 +00:00
return nil , xerrors . Errorf ( "unknown provisioner type %q" , provisionerType )
2024-05-03 15:14:26 +00:00
}
2022-05-19 22:47:45 +00:00
}
2023-06-22 00:21:40 +00:00
2024-01-10 11:29:57 +00:00
return provisionerd . New ( func ( dialCtx context . Context ) ( proto . DRPCProvisionerDaemonClient , error ) {
2022-11-10 22:37:33 +00:00
// This debounces calls to listen every second. Read the comment
// in provisionerdserver.go to learn more!
2024-05-03 15:14:26 +00:00
return coderAPI . CreateInMemoryProvisionerDaemon ( dialCtx , name , provisionerTypes )
2022-11-10 22:37:33 +00:00
} , & provisionerd . Options {
2023-12-07 16:59:13 +00:00
Logger : logger . Named ( fmt . Sprintf ( "provisionerd-%s" , name ) ) ,
2023-04-13 19:02:10 +00:00
UpdateInterval : time . Second ,
2023-03-07 21:10:01 +00:00
ForceCancelInterval : cfg . Provisioner . ForceCancelInterval . Value ( ) ,
2023-09-08 09:53:48 +00:00
Connector : connector ,
2022-11-08 13:19:40 +00:00
TracerProvider : coderAPI . TracerProvider ,
Metrics : & metrics ,
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
2024-03-15 16:24:38 +00:00
func PrintLogo ( inv * serpent . Invocation , daemonTitle string ) {
2023-01-13 02:08:23 +00:00
// Only print the logo in TTYs.
2023-03-23 22:42:20 +00:00
if ! isTTYOut ( inv ) {
2023-01-13 02:08:23 +00:00
return
}
2023-09-07 21:28:22 +00:00
versionString := cliui . Bold ( daemonTitle + " " + buildinfo . Version ( ) )
2023-07-12 12:07:36 +00:00
_ , _ = fmt . Fprintf ( inv . Stdout , "%s - Your Self-Hosted Remote Development Platform\n" , versionString )
2022-03-24 19:21:05 +00:00
}
2022-10-04 11:45:21 +00:00
func loadCertificates ( tlsCertFiles , tlsKeyFiles [ ] string ) ( [ ] tls . Certificate , error ) {
if len ( tlsCertFiles ) != len ( tlsKeyFiles ) {
return nil , xerrors . New ( "--tls-cert-file and --tls-key-file must be used the same amount of times" )
}
certs := make ( [ ] tls . Certificate , len ( tlsCertFiles ) )
for i := range tlsCertFiles {
certFile , keyFile := tlsCertFiles [ i ] , tlsKeyFiles [ i ]
cert , err := tls . LoadX509KeyPair ( certFile , keyFile )
if err != nil {
2023-03-07 21:10:01 +00:00
return nil , xerrors . Errorf (
"load TLS key pair %d (%q, %q): %w\ncertFiles: %+v\nkeyFiles: %+v" ,
i , certFile , keyFile , err ,
tlsCertFiles , tlsKeyFiles ,
)
2022-10-04 11:45:21 +00:00
}
certs [ i ] = cert
}
return certs , nil
}
2023-02-02 17:08:35 +00:00
// generateSelfSignedCertificate creates an unsafe self-signed certificate
// at random that allows users to proceed with setup in the event they
// haven't configured any TLS certificates.
func generateSelfSignedCertificate ( ) ( * tls . Certificate , error ) {
privateKey , err := ecdsa . GenerateKey ( elliptic . P256 ( ) , rand . Reader )
if err != nil {
return nil , err
}
template := x509 . Certificate {
SerialNumber : big . NewInt ( 1 ) ,
NotBefore : time . Now ( ) ,
NotAfter : time . Now ( ) . Add ( time . Hour * 24 * 180 ) ,
KeyUsage : x509 . KeyUsageKeyEncipherment | x509 . KeyUsageDigitalSignature ,
ExtKeyUsage : [ ] x509 . ExtKeyUsage { x509 . ExtKeyUsageServerAuth } ,
BasicConstraintsValid : true ,
IPAddresses : [ ] net . IP { net . ParseIP ( "127.0.0.1" ) } ,
}
derBytes , err := x509 . CreateCertificate ( rand . Reader , & template , & template , & privateKey . PublicKey , privateKey )
if err != nil {
return nil , err
}
var cert tls . Certificate
cert . Certificate = append ( cert . Certificate , derBytes )
cert . PrivateKey = privateKey
return & cert , nil
}
2023-11-07 14:55:39 +00:00
// configureServerTLS returns the TLS config used for the Coderd server
// connections to clients. A logger is passed in to allow printing warning
// messages that do not block startup.
//
//nolint:revive
func configureServerTLS ( ctx context . Context , logger slog . Logger , tlsMinVersion , tlsClientAuth string , tlsCertFiles , tlsKeyFiles [ ] string , tlsClientCAFile string , ciphers [ ] string , allowInsecureCiphers bool ) ( * tls . Config , error ) {
2022-03-25 00:20:13 +00:00
tlsConfig := & tls . Config {
MinVersion : tls . VersionTLS12 ,
2023-03-29 19:45:57 +00:00
NextProtos : [ ] string { "h2" , "http/1.1" } ,
2022-03-25 00:20:13 +00:00
}
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 )
}
2023-11-07 14:55:39 +00:00
// A custom set of supported ciphers.
if len ( ciphers ) > 0 {
cipherIDs , err := configureCipherSuites ( ctx , logger , ciphers , allowInsecureCiphers , tlsConfig . MinVersion , tls . VersionTLS13 )
if err != nil {
return nil , err
}
tlsConfig . CipherSuites = cipherIDs
}
2022-03-24 19:21:05 +00:00
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 )
}
2022-10-04 11:45:21 +00:00
certs , err := loadCertificates ( tlsCertFiles , tlsKeyFiles )
2022-03-24 19:21:05 +00:00
if err != nil {
2022-10-04 11:45:21 +00:00
return nil , xerrors . Errorf ( "load certificates: %w" , err )
2022-03-24 19:21:05 +00:00
}
2023-02-02 17:08:35 +00:00
if len ( certs ) == 0 {
selfSignedCertificate , err := generateSelfSignedCertificate ( )
if err != nil {
return nil , xerrors . Errorf ( "generate self signed certificate: %w" , err )
}
certs = append ( certs , * selfSignedCertificate )
}
2022-10-17 13:43:30 +00:00
tlsConfig . Certificates = certs
2022-10-04 11:45:21 +00:00
tlsConfig . GetCertificate = func ( hi * tls . ClientHelloInfo ) ( * tls . Certificate , error ) {
// If there's only one certificate, return it.
if len ( certs ) == 1 {
return & certs [ 0 ] , nil
}
// Expensively check which certificate matches the client hello.
for _ , cert := range certs {
cert := cert
if err := hi . SupportsCertificate ( & cert ) ; err == nil {
return & cert , nil
}
}
2022-03-24 19:21:05 +00:00
2022-10-04 11:45:21 +00:00
// Return the first certificate if we have one, or return nil so the
// server doesn't fail.
if len ( certs ) > 0 {
return & certs [ 0 ] , nil
}
return nil , nil //nolint:nilnil
}
2022-03-24 19:21:05 +00:00
2022-12-14 14:44:29 +00:00
err = configureCAPool ( tlsClientCAFile , tlsConfig )
if err != nil {
return nil , err
}
return tlsConfig , nil
}
2023-11-07 14:55:39 +00:00
//nolint:revive
func configureCipherSuites ( ctx context . Context , logger slog . Logger , ciphers [ ] string , allowInsecureCiphers bool , minTLS , maxTLS uint16 ) ( [ ] uint16 , error ) {
if minTLS > maxTLS {
return nil , xerrors . Errorf ( "minimum tls version (%s) cannot be greater than maximum tls version (%s)" , versionName ( minTLS ) , versionName ( maxTLS ) )
}
if minTLS >= tls . VersionTLS13 {
// The cipher suites config option is ignored for tls 1.3 and higher.
// So this user flag is a no-op if the min version is 1.3.
return nil , xerrors . Errorf ( "'--tls-ciphers' cannot be specified when using minimum tls version 1.3 or higher, %d ciphers found as input." , len ( ciphers ) )
}
// Configure the cipher suites which parses the strings and converts them
// to golang cipher suites.
supported , err := parseTLSCipherSuites ( ciphers )
if err != nil {
return nil , xerrors . Errorf ( "tls ciphers: %w" , err )
}
// allVersions is all tls versions the server supports.
// We enumerate these to ensure if ciphers are configured, at least
// 1 cipher for each version exists.
allVersions := make ( map [ uint16 ] bool )
for v := minTLS ; v <= maxTLS ; v ++ {
allVersions [ v ] = false
}
var insecure [ ] string
cipherIDs := make ( [ ] uint16 , 0 , len ( supported ) )
for _ , cipher := range supported {
if cipher . Insecure {
// Always show this warning, even if they have allowInsecureCiphers
// specified.
logger . Warn ( ctx , "insecure tls cipher specified for server use" , slog . F ( "cipher" , cipher . Name ) )
insecure = append ( insecure , cipher . Name )
}
// This is a warning message to tell the user if they are specifying
// a cipher that does not support the tls versions they have specified.
// This makes the cipher essentially a "noop" cipher.
if ! hasSupportedVersion ( minTLS , maxTLS , cipher . SupportedVersions ) {
versions := make ( [ ] string , 0 , len ( cipher . SupportedVersions ) )
for _ , sv := range cipher . SupportedVersions {
versions = append ( versions , versionName ( sv ) )
}
logger . Warn ( ctx , "cipher not supported for tls versions enabled, cipher will not be used" ,
slog . F ( "cipher" , cipher . Name ) ,
slog . F ( "cipher_supported_versions" , strings . Join ( versions , "," ) ) ,
slog . F ( "server_min_version" , versionName ( minTLS ) ) ,
slog . F ( "server_max_version" , versionName ( maxTLS ) ) ,
)
}
for _ , v := range cipher . SupportedVersions {
allVersions [ v ] = true
}
cipherIDs = append ( cipherIDs , cipher . ID )
}
if len ( insecure ) > 0 && ! allowInsecureCiphers {
return nil , xerrors . Errorf ( "insecure tls ciphers specified, must use '--tls-allow-insecure-ciphers' to allow these: %s" , strings . Join ( insecure , ", " ) )
}
// This is an additional sanity check. The user can specify ciphers that
// do not cover the full range of tls versions they have specified.
// They can unintentionally break TLS for some tls configured versions.
var missedVersions [ ] string
for version , covered := range allVersions {
if version == tls . VersionTLS13 {
continue // v1.3 ignores configured cipher suites.
}
if ! covered {
missedVersions = append ( missedVersions , versionName ( version ) )
}
}
if len ( missedVersions ) > 0 {
return nil , xerrors . Errorf ( "no tls ciphers supported for tls versions %q." +
"Add additional ciphers, set the minimum version to 'tls13, or remove the ciphers configured and rely on the default" ,
strings . Join ( missedVersions , "," ) )
}
return cipherIDs , nil
}
// parseTLSCipherSuites will parse cipher suite names like 'TLS_RSA_WITH_AES_128_CBC_SHA'
// to their tls cipher suite structs. If a cipher suite that is unsupported is
// passed in, this function will return an error.
// This function can return insecure cipher suites.
func parseTLSCipherSuites ( ciphers [ ] string ) ( [ ] tls . CipherSuite , error ) {
if len ( ciphers ) == 0 {
return nil , nil
}
var unsupported [ ] string
var supported [ ] tls . CipherSuite
// A custom set of supported ciphers.
allCiphers := append ( tls . CipherSuites ( ) , tls . InsecureCipherSuites ( ) ... )
for _ , cipher := range ciphers {
// For each cipher specified by the client, find the cipher in the
// list of golang supported ciphers.
var found * tls . CipherSuite
for _ , supported := range allCiphers {
if strings . EqualFold ( supported . Name , cipher ) {
found = supported
break
}
}
if found == nil {
unsupported = append ( unsupported , cipher )
continue
}
supported = append ( supported , * found )
}
if len ( unsupported ) > 0 {
return nil , xerrors . Errorf ( "unsupported tls ciphers specified, see https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L53-L75: %s" , strings . Join ( unsupported , ", " ) )
}
return supported , nil
}
// hasSupportedVersion is a helper function that returns true if the list
// of supported versions contains a version between min and max.
// If the versions list is outside the min/max, then it returns false.
func hasSupportedVersion ( min , max uint16 , versions [ ] uint16 ) bool {
for _ , v := range versions {
if v >= min && v <= max {
// If one version is in between min/max, return true.
return true
}
}
return false
}
// versionName is tls.VersionName in go 1.21.
// Until the switch, the function is copied locally.
func versionName ( version uint16 ) string {
switch version {
case tls . VersionSSL30 :
return "SSLv3"
case tls . VersionTLS10 :
return "TLS 1.0"
case tls . VersionTLS11 :
return "TLS 1.1"
case tls . VersionTLS12 :
return "TLS 1.2"
case tls . VersionTLS13 :
return "TLS 1.3"
default :
return fmt . Sprintf ( "0x%04X" , version )
}
}
2023-08-14 22:33:13 +00:00
func configureOIDCPKI ( orig * oauth2 . Config , keyFile string , certFile string ) ( * oauthpki . Config , error ) {
// Read the files
keyData , err := os . ReadFile ( keyFile )
if err != nil {
return nil , xerrors . Errorf ( "read oidc client key file: %w" , err )
}
var certData [ ] byte
// According to the spec, this is not required. So do not require it on the initial loading
// of the PKI config.
if certFile != "" {
certData , err = os . ReadFile ( certFile )
if err != nil {
return nil , xerrors . Errorf ( "read oidc client cert file: %w" , err )
}
}
return oauthpki . NewOauth2PKIConfig ( oauthpki . ConfigParams {
ClientID : orig . ClientID ,
TokenURL : orig . Endpoint . TokenURL ,
Scopes : orig . Scopes ,
PemEncodedKey : keyData ,
PemEncodedCert : certData ,
Config : orig ,
} )
}
2022-12-14 14:44:29 +00:00
func configureCAPool ( tlsClientCAFile string , tlsConfig * tls . Config ) error {
2022-03-24 19:21:05 +00:00
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 {
2022-12-14 14:44:29 +00:00
return xerrors . Errorf ( "read %q: %w" , tlsClientCAFile , err )
2022-03-24 19:21:05 +00:00
}
if ! caPool . AppendCertsFromPEM ( data ) {
2022-12-14 14:44:29 +00:00
return xerrors . Errorf ( "failed to parse CA certificate in tls-client-ca-file" )
2022-03-24 19:21:05 +00:00
}
tlsConfig . ClientCAs = caPool
}
2022-12-14 14:44:29 +00:00
return nil
2022-03-24 19:21:05 +00:00
}
2022-04-18 17:37:01 +00:00
2022-11-15 16:56:46 +00:00
//nolint:revive // Ignore flag-parameter: parameter 'allowEveryone' seems to be a control flag, avoid control coupling (revive)
2024-01-10 15:13:30 +00:00
func configureGithubOAuth2 ( instrument * promoauth . Factory , accessURL * url . URL , clientID , clientSecret string , allowSignups , allowEveryone 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-11-15 16:56:46 +00:00
if allowEveryone && len ( allowOrgs ) > 0 {
return nil , xerrors . New ( "allow everyone and allowed orgs cannot be used together" )
}
if allowEveryone && len ( rawTeams ) > 0 {
return nil , xerrors . New ( "allow everyone and allowed teams cannot be used together" )
}
if ! allowEveryone && len ( allowOrgs ) == 0 {
return nil , xerrors . New ( "allowed orgs is empty: must specify at least one org or allow everyone" )
}
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
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 ( ) ,
}
}
2024-01-26 00:34:46 +00:00
instrumentedOauth := instrument . NewGithub ( "github-login" , & oauth2 . Config {
ClientID : clientID ,
ClientSecret : clientSecret ,
Endpoint : endpoint ,
RedirectURL : redirectURL . String ( ) ,
Scopes : [ ] string {
"read:user" ,
"read:org" ,
"user:email" ,
} ,
} )
createClient := func ( client * http . Client , source promoauth . Oauth2Source ) ( * github . Client , error ) {
client = instrumentedOauth . InstrumentHTTPClient ( client , source )
if enterpriseBaseURL != "" {
return github . NewEnterpriseClient ( enterpriseBaseURL , "" , client )
}
return github . NewClient ( client ) , nil
}
2022-04-23 22:58:57 +00:00
return & coderd . GithubOAuth2Config {
2024-01-26 00:34:46 +00:00
OAuth2Config : instrumentedOauth ,
2022-04-23 22:58:57 +00:00
AllowSignups : allowSignups ,
2022-11-15 16:56:46 +00:00
AllowEveryone : allowEveryone ,
2022-04-23 22:58:57 +00:00
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 ) {
2024-01-26 00:34:46 +00:00
api , err := createClient ( client , promoauth . SourceGitAPIAuthUser )
2022-08-09 01:49:51 +00:00
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 ) {
2024-01-26 00:34:46 +00:00
api , err := createClient ( client , promoauth . SourceGitAPIListEmails )
2022-08-09 01:49:51 +00:00
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 ) {
2024-01-26 00:34:46 +00:00
api , err := createClient ( client , promoauth . SourceGitAPIOrgMemberships )
2022-08-09 01:49:51 +00:00
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 ) {
2024-01-26 00:34:46 +00:00
api , err := createClient ( client , promoauth . SourceGitAPITeamMemberships )
2022-08-09 01:49:51 +00:00
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-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 )
}
2022-12-19 19:25:59 +00:00
pgPort , err := strconv . ParseUint ( pgPortRaw , 10 , 16 )
2022-06-15 21:02:18 +00:00
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
}
2022-11-13 20:15:06 +00:00
2023-04-13 14:07:19 +00:00
func ConfigureHTTPClient ( ctx context . Context , clientCertFile , clientKeyFile string , tlsClientCAFile string ) ( context . Context , * http . Client , error ) {
2022-12-14 14:44:29 +00:00
if clientCertFile != "" && clientKeyFile != "" {
certificates , err := loadCertificates ( [ ] string { clientCertFile } , [ ] string { clientKeyFile } )
2022-11-13 20:15:06 +00:00
if err != nil {
2022-12-14 14:44:29 +00:00
return ctx , nil , err
2022-11-13 20:15:06 +00:00
}
2022-12-14 14:44:29 +00:00
tlsClientConfig := & tls . Config { //nolint:gosec
Certificates : certificates ,
2023-03-29 19:45:57 +00:00
NextProtos : [ ] string { "h2" , "http/1.1" } ,
2022-12-14 14:44:29 +00:00
}
err = configureCAPool ( tlsClientCAFile , tlsClientConfig )
if err != nil {
return nil , nil , err
}
httpClient := & http . Client {
2022-11-13 20:15:06 +00:00
Transport : & http . Transport {
2022-12-14 14:44:29 +00:00
TLSClientConfig : tlsClientConfig ,
2022-11-13 20:15:06 +00:00
} ,
2022-12-14 14:44:29 +00:00
}
return context . WithValue ( ctx , oauth2 . HTTPClient , httpClient ) , httpClient , nil
2022-11-13 20:15:06 +00:00
}
2022-12-14 14:44:29 +00:00
return ctx , & http . Client { } , nil
2022-11-13 20:15:06 +00:00
}
2022-12-15 20:09:19 +00:00
2023-02-02 17:08:35 +00:00
// nolint:revive
2023-02-08 19:48:17 +00:00
func redirectToAccessURL ( handler http . Handler , accessURL * url . URL , tunnel bool , appHostnameRegex * regexp . Regexp ) http . Handler {
2022-12-15 20:09:19 +00:00
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
2023-02-02 17:08:35 +00:00
redirect := func ( ) {
2022-12-15 20:09:19 +00:00
http . Redirect ( w , r , accessURL . String ( ) , http . StatusTemporaryRedirect )
2023-02-02 17:08:35 +00:00
}
2024-02-09 13:44:47 +00:00
// Exception: /healthz
// Kubernetes doesn't like it if you redirect your healthcheck or liveness check endpoint.
if r . URL . Path == "/healthz" {
handler . ServeHTTP ( w , r )
return
}
2023-11-20 10:46:59 +00:00
// Exception: DERP
// We use this endpoint when creating a DERP-mesh in the enterprise version to directly
// dial other Coderd derpers. Redirecting to the access URL breaks direct dial since the
// access URL will be load-balanced in a multi-replica deployment.
//
// It's totally fine to access DERP over TLS, but we also don't need to redirect HTTP to
// HTTPS as DERP is itself an encrypted protocol.
if isDERPPath ( r . URL . Path ) {
handler . ServeHTTP ( w , r )
return
}
2023-02-02 17:08:35 +00:00
// Only do this if we aren't tunneling.
// If we are tunneling, we want to allow the request to go through
// because the tunnel doesn't proxy with TLS.
if ! tunnel && accessURL . Scheme == "https" && r . TLS == nil {
redirect ( )
return
}
2023-02-08 19:48:17 +00:00
if r . Host == accessURL . Host {
handler . ServeHTTP ( w , r )
return
}
2023-04-06 17:07:24 +00:00
if r . Header . Get ( "X-Forwarded-Host" ) == accessURL . Host {
handler . ServeHTTP ( w , r )
return
}
2023-02-08 19:48:17 +00:00
if appHostnameRegex != nil && appHostnameRegex . MatchString ( r . Host ) {
handler . ServeHTTP ( w , r )
2022-12-15 20:09:19 +00:00
return
}
2023-02-08 19:48:17 +00:00
redirect ( )
2022-12-15 20:09:19 +00:00
} )
}
2022-12-19 19:25:59 +00:00
2023-11-20 10:46:59 +00:00
func isDERPPath ( p string ) bool {
segments := strings . SplitN ( p , "/" , 3 )
if len ( segments ) < 2 {
return false
}
return segments [ 1 ] == "derp"
}
2023-04-13 14:07:19 +00:00
// IsLocalhost returns true if the host points to the local machine. Intended to
2022-12-19 19:25:59 +00:00
// be called with `u.Hostname()`.
2023-04-13 14:07:19 +00:00
func IsLocalhost ( host string ) bool {
2022-12-19 19:25:59 +00:00
return host == "localhost" || host == "127.0.0.1" || host == "::1"
}
2023-01-13 02:08:23 +00:00
2023-10-05 14:57:48 +00:00
func ConnectToPostgres ( ctx context . Context , logger slog . Logger , driver string , dbURL string ) ( sqlDB * sql . DB , err error ) {
2023-02-06 14:58:21 +00:00
logger . Debug ( ctx , "connecting to postgresql" )
2023-04-28 18:51:31 +00:00
// Try to connect for 30 seconds.
ctx , cancel := context . WithTimeout ( ctx , 30 * time . Second )
defer cancel ( )
2023-10-05 14:57:48 +00:00
defer func ( ) {
if err == nil {
return
}
if sqlDB != nil {
_ = sqlDB . Close ( )
sqlDB = nil
}
logger . Error ( ctx , "connect to postgres failed" , slog . Error ( err ) )
} ( )
var tries int
2023-04-28 18:51:31 +00:00
for r := retry . New ( time . Second , 3 * time . Second ) ; r . Wait ( ctx ) ; {
tries ++
sqlDB , err = sql . Open ( driver , dbURL )
if err != nil {
2023-10-05 14:57:48 +00:00
logger . Warn ( ctx , "connect to postgres: retrying" , slog . Error ( err ) , slog . F ( "try" , tries ) )
2023-04-28 18:51:31 +00:00
continue
}
err = pingPostgres ( ctx , sqlDB )
if err != nil {
2023-10-05 14:57:48 +00:00
logger . Warn ( ctx , "ping postgres: retrying" , slog . Error ( err ) , slog . F ( "try" , tries ) )
_ = sqlDB . Close ( )
sqlDB = nil
2023-04-28 18:51:31 +00:00
continue
}
break
}
2023-10-05 14:57:48 +00:00
if err == nil {
err = ctx . Err ( )
}
2023-02-06 14:58:21 +00:00
if err != nil {
2023-10-05 14:57:48 +00:00
return nil , xerrors . Errorf ( "unable to connect after %d tries; last error: %w" , tries , err )
2023-02-06 14:58:21 +00:00
}
// Ensure the PostgreSQL version is >=13.0.0!
2023-04-19 16:59:56 +00:00
version , err := sqlDB . QueryContext ( ctx , "SHOW server_version_num;" )
2023-02-06 14:58:21 +00:00
if err != nil {
return nil , xerrors . Errorf ( "get postgres version: %w" , err )
}
if ! version . Next ( ) {
return nil , xerrors . Errorf ( "no rows returned for version select" )
}
2023-04-19 16:59:56 +00:00
var versionNum int
err = version . Scan ( & versionNum )
2023-02-06 14:58:21 +00:00
if err != nil {
return nil , xerrors . Errorf ( "scan version: %w" , err )
}
_ = version . Close ( )
2023-04-19 16:59:56 +00:00
if versionNum < 130000 {
return nil , xerrors . Errorf ( "PostgreSQL version must be v13.0.0 or higher! Got: %d" , versionNum )
2023-02-06 14:58:21 +00:00
}
2023-04-19 16:59:56 +00:00
logger . Debug ( ctx , "connected to postgresql" , slog . F ( "version" , versionNum ) )
2023-02-06 14:58:21 +00:00
err = migrations . Up ( sqlDB )
if err != nil {
return nil , xerrors . Errorf ( "migrate up: %w" , err )
}
// The default is 0 but the request will fail with a 500 if the DB
// cannot accept new connections, so we try to limit that here.
// Requests will wait for a new connection instead of a hard error
// if a limit is set.
sqlDB . SetMaxOpenConns ( 10 )
// Allow a max of 3 idle connections at a time. Lower values end up
// creating a lot of connection churn. Since each connection uses about
// 10MB of memory, we're allocating 30MB to Postgres connections per
// replica, but is better than causing Postgres to spawn a thread 15-20
// times/sec. PGBouncer's transaction pooling is not the greatest so
// it's not optimal for us to deploy.
//
// This was set to 10 before we started doing HA deployments, but 3 was
// later determined to be a better middle ground as to not use up all
// of PGs default connection limit while simultaneously avoiding a lot
// of connection churn.
sqlDB . SetMaxIdleConns ( 3 )
return sqlDB , nil
}
2023-04-13 14:07:19 +00:00
2023-04-28 18:51:31 +00:00
func pingPostgres ( ctx context . Context , db * sql . DB ) error {
ctx , cancel := context . WithTimeout ( ctx , 5 * time . Second )
defer cancel ( )
return db . PingContext ( ctx )
}
2023-04-13 14:07:19 +00:00
type HTTPServers struct {
HTTPUrl * url . URL
HTTPListener net . Listener
// TLS
TLSUrl * url . URL
TLSListener net . Listener
TLSConfig * tls . Config
}
// Serve acts just like http.Serve. It is a blocking call until the server
// is closed, and an error is returned if any underlying Serve call fails.
func ( s * HTTPServers ) Serve ( srv * http . Server ) error {
eg := errgroup . Group { }
if s . HTTPListener != nil {
eg . Go ( func ( ) error {
defer s . Close ( ) // close all listeners on error
return srv . Serve ( s . HTTPListener )
} )
}
if s . TLSListener != nil {
eg . Go ( func ( ) error {
defer s . Close ( ) // close all listeners on error
return srv . Serve ( s . TLSListener )
} )
}
return eg . Wait ( )
}
func ( s * HTTPServers ) Close ( ) {
if s . HTTPListener != nil {
_ = s . HTTPListener . Close ( )
}
if s . TLSListener != nil {
_ = s . TLSListener . Close ( )
}
}
2023-05-02 17:06:58 +00:00
func ConfigureTraceProvider (
ctx context . Context ,
logger slog . Logger ,
cfg * codersdk . DeploymentValues ,
) ( trace . TracerProvider , string , func ( context . Context ) error ) {
2023-04-13 14:07:19 +00:00
var (
2023-05-02 17:06:58 +00:00
tracerProvider = trace . NewNoopTracerProvider ( )
closeTracing = func ( context . Context ) error { return nil }
2023-04-13 14:07:19 +00:00
sqlDriver = "postgres"
)
2023-10-18 20:08:02 +00:00
otel . SetTextMapPropagator (
propagation . NewCompositeTextMapPropagator (
propagation . TraceContext { } ,
propagation . Baggage { } ,
) ,
)
2023-09-14 06:20:28 +00:00
if cfg . Trace . Enable . Value ( ) || cfg . Trace . DataDog . Value ( ) || cfg . Trace . HoneycombAPIKey != "" {
2023-05-02 17:06:58 +00:00
sdkTracerProvider , _closeTracing , err := tracing . TracerProvider ( ctx , "coderd" , tracing . TracerOpts {
2023-04-13 14:07:19 +00:00
Default : cfg . Trace . Enable . Value ( ) ,
2023-08-29 22:14:28 +00:00
DataDog : cfg . Trace . DataDog . Value ( ) ,
2023-04-13 14:07:19 +00:00
Honeycomb : cfg . Trace . HoneycombAPIKey . String ( ) ,
} )
if err != nil {
logger . Warn ( ctx , "start telemetry exporter" , slog . Error ( err ) )
} else {
d , err := tracing . PostgresDriver ( sdkTracerProvider , "coderd.database" )
if err != nil {
logger . Warn ( ctx , "start postgres tracing driver" , slog . Error ( err ) )
} else {
sqlDriver = d
}
tracerProvider = sdkTracerProvider
2023-05-02 17:06:58 +00:00
closeTracing = _closeTracing
2023-04-13 14:07:19 +00:00
}
}
2023-05-02 17:06:58 +00:00
return tracerProvider , sqlDriver , closeTracing
2023-04-13 14:07:19 +00:00
}
2024-03-15 16:24:38 +00:00
func ConfigureHTTPServers ( logger slog . Logger , inv * serpent . Invocation , cfg * codersdk . DeploymentValues ) ( _ * HTTPServers , err error ) {
2023-11-07 14:55:39 +00:00
ctx := inv . Context ( )
2023-04-13 14:07:19 +00:00
httpServers := & HTTPServers { }
defer func ( ) {
if err != nil {
// Always close the listeners if we fail.
httpServers . Close ( )
}
} ( )
// Validate bind addresses.
if cfg . Address . String ( ) != "" {
if cfg . TLS . Enable {
cfg . HTTPAddress = ""
cfg . TLS . Address = cfg . Address
} else {
_ = cfg . HTTPAddress . Set ( cfg . Address . String ( ) )
cfg . TLS . Address . Host = ""
cfg . TLS . Address . Port = ""
}
}
if cfg . TLS . Enable && cfg . TLS . Address . String ( ) == "" {
return nil , xerrors . Errorf ( "TLS address must be set if TLS is enabled" )
}
if ! cfg . TLS . Enable && cfg . HTTPAddress . String ( ) == "" {
return nil , xerrors . Errorf ( "TLS is disabled. Enable with --tls-enable or specify a HTTP address" )
}
if cfg . AccessURL . String ( ) != "" &&
! ( cfg . AccessURL . Scheme == "http" || cfg . AccessURL . Scheme == "https" ) {
return nil , xerrors . Errorf ( "access-url must include a scheme (e.g. 'http://' or 'https://)" )
}
addrString := func ( l net . Listener ) string {
listenAddrStr := l . Addr ( ) . String ( )
// For some reason if 0.0.0.0:x is provided as the https
// address, httpsListener.Addr().String() likes to return it as
// an ipv6 address (i.e. [::]:x). If the input ip is 0.0.0.0,
// try to coerce the output back to ipv4 to make it less
// confusing.
if strings . Contains ( cfg . HTTPAddress . String ( ) , "0.0.0.0" ) {
listenAddrStr = strings . ReplaceAll ( listenAddrStr , "[::]" , "0.0.0.0" )
}
return listenAddrStr
}
if cfg . HTTPAddress . String ( ) != "" {
httpServers . HTTPListener , err = net . Listen ( "tcp" , cfg . HTTPAddress . String ( ) )
if err != nil {
return nil , err
}
// We want to print out the address the user supplied, not the
// loopback device.
_ , _ = fmt . Fprintf ( inv . Stdout , "Started HTTP listener at %s\n" , ( & url . URL { Scheme : "http" , Host : addrString ( httpServers . HTTPListener ) } ) . String ( ) )
// Set the http URL we want to use when connecting to ourselves.
tcpAddr , tcpAddrValid := httpServers . HTTPListener . Addr ( ) . ( * net . TCPAddr )
if ! tcpAddrValid {
return nil , xerrors . Errorf ( "invalid TCP address type %T" , httpServers . HTTPListener . Addr ( ) )
}
if tcpAddr . IP . IsUnspecified ( ) {
tcpAddr . IP = net . IPv4 ( 127 , 0 , 0 , 1 )
}
httpServers . HTTPUrl = & url . URL {
Scheme : "http" ,
Host : tcpAddr . String ( ) ,
}
}
if cfg . TLS . Enable {
if cfg . TLS . Address . String ( ) == "" {
return nil , xerrors . New ( "tls address must be set if tls is enabled" )
}
2023-11-17 11:09:29 +00:00
redirectHTTPToHTTPSDeprecation ( ctx , logger , inv , cfg )
2023-04-13 14:07:19 +00:00
2023-11-07 14:55:39 +00:00
tlsConfig , err := configureServerTLS (
ctx ,
logger ,
2023-04-13 14:07:19 +00:00
cfg . TLS . MinVersion . String ( ) ,
cfg . TLS . ClientAuth . String ( ) ,
cfg . TLS . CertFiles ,
cfg . TLS . KeyFiles ,
cfg . TLS . ClientCAFile . String ( ) ,
2023-11-07 14:55:39 +00:00
cfg . TLS . SupportedCiphers . Value ( ) ,
cfg . TLS . AllowInsecureCiphers . Value ( ) ,
2023-04-13 14:07:19 +00:00
)
if err != nil {
return nil , xerrors . Errorf ( "configure tls: %w" , err )
}
httpsListenerInner , err := net . Listen ( "tcp" , cfg . TLS . Address . String ( ) )
if err != nil {
return nil , err
}
httpServers . TLSConfig = tlsConfig
httpServers . TLSListener = tls . NewListener ( httpsListenerInner , tlsConfig )
// We want to print out the address the user supplied, not the
// loopback device.
_ , _ = fmt . Fprintf ( inv . Stdout , "Started TLS/HTTPS listener at %s\n" , ( & url . URL { Scheme : "https" , Host : addrString ( httpServers . TLSListener ) } ) . String ( ) )
// Set the https URL we want to use when connecting to
// ourselves.
tcpAddr , tcpAddrValid := httpServers . TLSListener . Addr ( ) . ( * net . TCPAddr )
if ! tcpAddrValid {
return nil , xerrors . Errorf ( "invalid TCP address type %T" , httpServers . TLSListener . Addr ( ) )
}
if tcpAddr . IP . IsUnspecified ( ) {
tcpAddr . IP = net . IPv4 ( 127 , 0 , 0 , 1 )
}
httpServers . TLSUrl = & url . URL {
Scheme : "https" ,
Host : tcpAddr . String ( ) ,
}
}
if httpServers . HTTPListener == nil && httpServers . TLSListener == nil {
return nil , xerrors . New ( "must listen on at least one address" )
}
return httpServers , nil
}
2023-10-03 14:04:39 +00:00
2023-11-17 11:09:29 +00:00
// redirectHTTPToHTTPSDeprecation handles deprecation of the --tls-redirect-http-to-https flag and
// "related" environment variables.
//
// --tls-redirect-http-to-https used to default to true.
// It made more sense to have the redirect be opt-in.
//
// Also, for a while we have been accepting the environment variable (but not the
// corresponding flag!) "CODER_TLS_REDIRECT_HTTP", and it appeared in a configuration
// example, so we keep accepting it to not break backward compat.
2024-03-15 16:24:38 +00:00
func redirectHTTPToHTTPSDeprecation ( ctx context . Context , logger slog . Logger , inv * serpent . Invocation , cfg * codersdk . DeploymentValues ) {
2023-11-17 11:09:29 +00:00
truthy := func ( s string ) bool {
b , err := strconv . ParseBool ( s )
if err != nil {
return false
}
return b
}
if truthy ( inv . Environ . Get ( "CODER_TLS_REDIRECT_HTTP" ) ) ||
truthy ( inv . Environ . Get ( "CODER_TLS_REDIRECT_HTTP_TO_HTTPS" ) ) ||
inv . ParsedFlags ( ) . Changed ( "tls-redirect-http-to-https" ) {
logger . Warn ( ctx , "⚠️ --tls-redirect-http-to-https is deprecated, please use --redirect-to-access-url instead" )
cfg . RedirectToAccessURL = cfg . TLS . RedirectHTTP
}
}
2023-10-03 14:04:39 +00:00
// ReadExternalAuthProvidersFromEnv is provided for compatibility purposes with
// the viper CLI.
func ReadExternalAuthProvidersFromEnv ( environ [ ] string ) ( [ ] codersdk . ExternalAuthConfig , error ) {
providers , err := parseExternalAuthProvidersFromEnv ( "CODER_EXTERNAL_AUTH_" , environ )
if err != nil {
return nil , err
}
// Deprecated: To support legacy git auth!
gitProviders , err := parseExternalAuthProvidersFromEnv ( "CODER_GITAUTH_" , environ )
if err != nil {
return nil , err
}
return append ( providers , gitProviders ... ) , nil
}
// parseExternalAuthProvidersFromEnv consumes environment variables to parse
// external auth providers. A prefix is provided to support the legacy
// parsing of `GITAUTH` environment variables.
func parseExternalAuthProvidersFromEnv ( prefix string , environ [ ] string ) ( [ ] codersdk . ExternalAuthConfig , error ) {
// The index numbers must be in-order.
sort . Strings ( environ )
var providers [ ] codersdk . ExternalAuthConfig
2024-03-15 16:24:38 +00:00
for _ , v := range serpent . ParseEnviron ( environ , prefix ) {
2023-10-03 14:04:39 +00:00
tokens := strings . SplitN ( v . Name , "_" , 2 )
if len ( tokens ) != 2 {
return nil , xerrors . Errorf ( "invalid env var: %s" , v . Name )
}
providerNum , err := strconv . Atoi ( tokens [ 0 ] )
if err != nil {
return nil , xerrors . Errorf ( "parse number: %s" , v . Name )
}
var provider codersdk . ExternalAuthConfig
switch {
case len ( providers ) < providerNum :
return nil , xerrors . Errorf (
"provider num %v skipped: %s" ,
len ( providers ) ,
v . Name ,
)
case len ( providers ) == providerNum :
// At the next next provider.
providers = append ( providers , provider )
case len ( providers ) == providerNum + 1 :
// At the current provider.
provider = providers [ providerNum ]
}
key := tokens [ 1 ]
switch key {
case "ID" :
provider . ID = v . Value
case "TYPE" :
provider . Type = v . Value
case "CLIENT_ID" :
provider . ClientID = v . Value
case "CLIENT_SECRET" :
provider . ClientSecret = v . Value
case "AUTH_URL" :
provider . AuthURL = v . Value
case "TOKEN_URL" :
provider . TokenURL = v . Value
case "VALIDATE_URL" :
provider . ValidateURL = v . Value
case "REGEX" :
provider . Regex = v . Value
case "DEVICE_FLOW" :
b , err := strconv . ParseBool ( v . Value )
if err != nil {
return nil , xerrors . Errorf ( "parse bool: %s" , v . Value )
}
provider . DeviceFlow = b
case "DEVICE_CODE_URL" :
provider . DeviceCodeURL = v . Value
case "NO_REFRESH" :
b , err := strconv . ParseBool ( v . Value )
if err != nil {
return nil , xerrors . Errorf ( "parse bool: %s" , v . Value )
}
provider . NoRefresh = b
case "SCOPES" :
provider . Scopes = strings . Split ( v . Value , " " )
2023-10-09 23:49:30 +00:00
case "EXTRA_TOKEN_KEYS" :
provider . ExtraTokenKeys = strings . Split ( v . Value , " " )
2023-10-03 14:04:39 +00:00
case "APP_INSTALL_URL" :
provider . AppInstallURL = v . Value
case "APP_INSTALLATIONS_URL" :
provider . AppInstallationsURL = v . Value
case "DISPLAY_NAME" :
provider . DisplayName = v . Value
case "DISPLAY_ICON" :
provider . DisplayIcon = v . Value
}
providers [ providerNum ] = provider
}
return providers , nil
}
2024-01-04 13:46:00 +00:00
// If the user provides a postgres URL with a password that contains special
// characters, the URL will be invalid. We need to escape the password so that
// the URL parse doesn't fail at the DB connector level.
func escapePostgresURLUserInfo ( v string ) ( string , error ) {
_ , err := url . Parse ( v )
// I wish I could use errors.Is here, but this error is not declared as a
// variable in net/url. :(
if err != nil {
if strings . Contains ( err . Error ( ) , "net/url: invalid userinfo" ) {
// If the URL is invalid, we assume it is because the password contains
// special characters that need to be escaped.
// get everything before first @
parts := strings . SplitN ( v , "@" , 2 )
if len ( parts ) != 2 {
return "" , xerrors . Errorf ( "invalid postgres url with userinfo: %s" , v )
}
start := parts [ 0 ]
// get password, which is the last item in start when split by :
startParts := strings . Split ( start , ":" )
password := startParts [ len ( startParts ) - 1 ]
// escape password, and replace the last item in the startParts slice
// with the escaped password.
//
// url.PathEscape is used here because url.QueryEscape
// will not escape spaces correctly.
newPassword := url . PathEscape ( password )
startParts [ len ( startParts ) - 1 ] = newPassword
start = strings . Join ( startParts , ":" )
return start + "@" + parts [ 1 ] , nil
}
return "" , xerrors . Errorf ( "parse postgres url: %w" , err )
}
return v , nil
}
2024-03-15 13:16:36 +00:00
2024-03-15 16:24:38 +00:00
func signalNotifyContext ( ctx context . Context , inv * serpent . Invocation , sig ... os . Signal ) ( context . Context , context . CancelFunc ) {
2024-03-15 13:16:36 +00:00
// On Windows, some of our signal functions lack support.
// If we pass in no signals, we should just return the context as-is.
if len ( sig ) == 0 {
return context . WithCancel ( ctx )
}
return inv . SignalNotifyContext ( ctx , sig ... )
}
2024-03-20 17:14:43 +00:00
func getPostgresDB ( ctx context . Context , logger slog . Logger , postgresURL string , auth codersdk . PostgresAuth , sqlDriver string ) ( * sql . DB , string , error ) {
dbURL , err := escapePostgresURLUserInfo ( postgresURL )
if err != nil {
return nil , "" , xerrors . Errorf ( "escaping postgres URL: %w" , err )
}
if auth == codersdk . PostgresAuthAWSIAMRDS {
sqlDriver , err = awsiamrds . Register ( ctx , sqlDriver )
if err != nil {
return nil , "" , xerrors . Errorf ( "register aws rds iam auth: %w" , err )
}
}
sqlDB , err := ConnectToPostgres ( ctx , logger , sqlDriver , dbURL )
if err != nil {
return nil , "" , xerrors . Errorf ( "connect to postgres: %w" , err )
}
return sqlDB , dbURL , nil
}