feat: allow http and https listening simultaneously (#5365)

This commit is contained in:
Dean Sheather
2022-12-16 06:09:19 +10:00
committed by GitHub
parent 787b8b2a51
commit 31d38d4246
15 changed files with 692 additions and 136 deletions

View File

@ -32,12 +32,22 @@ func newConfig() *codersdk.DeploymentConfig {
Usage: "Specifies the wildcard hostname to use for workspace applications in the form \"*.example.com\".", Usage: "Specifies the wildcard hostname to use for workspace applications in the form \"*.example.com\".",
Flag: "wildcard-access-url", Flag: "wildcard-access-url",
}, },
// DEPRECATED: Use HTTPAddress or TLS.Address instead.
Address: &codersdk.DeploymentConfigField[string]{ Address: &codersdk.DeploymentConfigField[string]{
Name: "Address", Name: "Address",
Usage: "Bind address of the server.", Usage: "Bind address of the server.",
Flag: "address", Flag: "address",
Shorthand: "a", Shorthand: "a",
Default: "127.0.0.1:3000", // Deprecated, so we don't have a default. If set, it will overwrite
// HTTPAddress and TLS.Address and print a warning.
Hidden: true,
Default: "",
},
HTTPAddress: &codersdk.DeploymentConfigField[string]{
Name: "Address",
Usage: "HTTP bind address of the server. Unset to disable the HTTP endpoint.",
Flag: "http-address",
Default: "127.0.0.1:3000",
}, },
AutobuildPollInterval: &codersdk.DeploymentConfigField[time.Duration]{ AutobuildPollInterval: &codersdk.DeploymentConfigField[time.Duration]{
Name: "Autobuild Poll Interval", Name: "Autobuild Poll Interval",
@ -267,6 +277,18 @@ func newConfig() *codersdk.DeploymentConfig {
Usage: "Whether TLS will be enabled.", Usage: "Whether TLS will be enabled.",
Flag: "tls-enable", Flag: "tls-enable",
}, },
Address: &codersdk.DeploymentConfigField[string]{
Name: "TLS Address",
Usage: "HTTPS bind address of the server.",
Flag: "tls-address",
Default: "127.0.0.1:3443",
},
RedirectHTTP: &codersdk.DeploymentConfigField[bool]{
Name: "Redirect HTTP to HTTPS",
Usage: "Whether HTTP requests will be redirected to the access URL (if it's a https URL and TLS is enabled). Requests to local IP addresses are never redirected regardless of this setting.",
Flag: "tls-redirect-http-to-https",
Default: true,
},
CertFiles: &codersdk.DeploymentConfigField[[]string]{ CertFiles: &codersdk.DeploymentConfigField[[]string]{
Name: "TLS Certificate Files", Name: "TLS Certificate Files",
Usage: "Path to each certificate for TLS. It requires a PEM-encoded file. To configure the listener to use a CA certificate, concatenate the primary certificate and the CA certificate together. The primary certificate should appear first in the combined file.", Usage: "Path to each certificate for TLS. It requires a PEM-encoded file. To configure the listener to use a CA certificate, concatenate the primary certificate and the CA certificate together. The primary certificate should appear first in the combined file.",

View File

@ -40,7 +40,7 @@ func TestResetPassword(t *testing.T) {
serverDone := make(chan struct{}) serverDone := make(chan struct{})
serverCmd, cfg := clitest.New(t, serverCmd, cfg := clitest.New(t,
"server", "server",
"--address", ":0", "--http-address", ":0",
"--access-url", "http://example.com", "--access-url", "http://example.com",
"--postgres-url", connectionURL, "--postgres-url", connectionURL,
"--cache-dir", t.TempDir(), "--cache-dir", t.TempDir(),

View File

@ -85,6 +85,25 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
if err != nil { if err != nil {
return xerrors.Errorf("getting deployment config: %w", err) return xerrors.Errorf("getting deployment config: %w", err)
} }
// Validate bind addresses.
if cfg.Address.Value != "" {
cmd.PrintErr(cliui.Styles.Warn.Render("WARN:") + " --address and -a are deprecated, please use --http-address and --tls-address instead")
if cfg.TLS.Enable.Value {
cfg.HTTPAddress.Value = ""
cfg.TLS.Address.Value = cfg.Address.Value
} else {
cfg.HTTPAddress.Value = cfg.Address.Value
cfg.TLS.Address.Value = ""
}
}
if cfg.TLS.Enable.Value && cfg.TLS.Address.Value == "" {
return xerrors.Errorf("TLS address must be set if TLS is enabled")
}
if !cfg.TLS.Enable.Value && cfg.HTTPAddress.Value == "" {
return xerrors.Errorf("either HTTP or TLS must be enabled")
}
printLogo(cmd) printLogo(cmd)
logger := slog.Make(sloghuman.Sink(cmd.ErrOrStderr())) logger := slog.Make(sloghuman.Sink(cmd.ErrOrStderr()))
if ok, _ := cmd.Flags().GetBool(varVerbose); ok { if ok, _ := cmd.Flags().GetBool(varVerbose); ok {
@ -186,14 +205,41 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
}() }()
} }
listener, err := net.Listen("tcp", cfg.Address.Value) var (
if err != nil { httpListener net.Listener
return xerrors.Errorf("listen %q: %w", cfg.Address.Value, err) httpURL *url.URL
} )
defer listener.Close() if cfg.HTTPAddress.Value != "" {
httpListener, err = net.Listen("tcp", cfg.HTTPAddress.Value)
if err != nil {
return xerrors.Errorf("listen %q: %w", cfg.HTTPAddress.Value, err)
}
defer httpListener.Close()
var tlsConfig *tls.Config tcpAddr, tcpAddrValid := httpListener.Addr().(*net.TCPAddr)
if !tcpAddrValid {
return xerrors.Errorf("invalid TCP address type %T", httpListener.Addr())
}
if tcpAddr.IP.IsUnspecified() {
tcpAddr.IP = net.IPv4(127, 0, 0, 1)
}
httpURL = &url.URL{
Scheme: "http",
Host: tcpAddr.String(),
}
cmd.Println("Started HTTP listener at " + httpURL.String())
}
var (
tlsConfig *tls.Config
httpsListener net.Listener
httpsURL *url.URL
)
if cfg.TLS.Enable.Value { if cfg.TLS.Enable.Value {
if cfg.TLS.Address.Value == "" {
return xerrors.New("tls address must be set if tls is enabled")
}
tlsConfig, err = configureTLS( tlsConfig, err = configureTLS(
cfg.TLS.MinVersion.Value, cfg.TLS.MinVersion.Value,
cfg.TLS.ClientAuth.Value, cfg.TLS.ClientAuth.Value,
@ -204,7 +250,38 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
if err != nil { if err != nil {
return xerrors.Errorf("configure tls: %w", err) return xerrors.Errorf("configure tls: %w", err)
} }
listener = tls.NewListener(listener, tlsConfig) httpsListenerInner, err := net.Listen("tcp", cfg.TLS.Address.Value)
if err != nil {
return xerrors.Errorf("listen %q: %w", cfg.TLS.Address.Value, err)
}
defer httpsListenerInner.Close()
httpsListener = tls.NewListener(httpsListenerInner, tlsConfig)
defer httpsListener.Close()
tcpAddr, tcpAddrValid := httpsListener.Addr().(*net.TCPAddr)
if !tcpAddrValid {
return xerrors.Errorf("invalid TCP address type %T", httpsListener.Addr())
}
if tcpAddr.IP.IsUnspecified() {
tcpAddr.IP = net.IPv4(127, 0, 0, 1)
}
httpsURL = &url.URL{
Scheme: "https",
Host: tcpAddr.String(),
}
cmd.Println("Started TLS/HTTPS listener at " + httpsURL.String())
}
// Sanity check that at least one listener was started.
if httpListener == nil && httpsListener == nil {
return xerrors.New("must listen on at least one address")
}
// Prefer HTTP because it's less prone to TLS errors over localhost.
localURL := httpsURL
if httpURL != nil {
localURL = httpURL
} }
ctx, httpClient, err := configureHTTPClient( ctx, httpClient, err := configureHTTPClient(
@ -217,24 +294,6 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
return xerrors.Errorf("configure http client: %w", err) return xerrors.Errorf("configure http client: %w", err)
} }
tcpAddr, valid := listener.Addr().(*net.TCPAddr)
if !valid {
return xerrors.New("must be listening on tcp")
}
// If just a port is specified, assume localhost.
if tcpAddr.IP.IsUnspecified() {
tcpAddr.IP = net.IPv4(127, 0, 0, 1)
}
// If no access URL is specified, fallback to the
// bounds URL.
localURL := &url.URL{
Scheme: "http",
Host: tcpAddr.String(),
}
if cfg.TLS.Enable.Value {
localURL.Scheme = "https"
}
var ( var (
ctxTunnel, closeTunnel = context.WithCancel(ctx) ctxTunnel, closeTunnel = context.WithCancel(ctx)
tunnel *devtunnel.Tunnel tunnel *devtunnel.Tunnel
@ -289,6 +348,15 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
cmd.Printf("%s 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", cliui.Styles.Warn.Render("Warning:"), cliui.Styles.Field.Render(accessURLParsed.String()), reason) cmd.Printf("%s 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", cliui.Styles.Warn.Render("Warning:"), cliui.Styles.Field.Render(accessURLParsed.String()), reason)
} }
// Redirect from the HTTP listener to the access URL if:
// 1. The redirect flag is enabled.
// 2. HTTP listening is enabled (obviously).
// 3. TLS is enabled (otherwise they're likely using a reverse proxy
// which can do this instead).
// 4. The access URL has been set manually (not a tunnel).
// 5. The access URL is HTTPS.
shouldRedirectHTTPToAccessURL := cfg.TLS.RedirectHTTP.Value && cfg.HTTPAddress.Value != "" && cfg.TLS.Enable.Value && tunnel == nil && accessURLParsed.Scheme == "https"
// A newline is added before for visibility in terminal output. // A newline is added before for visibility in terminal output.
cmd.Printf("\nView the Web UI: %s\n", accessURLParsed.String()) cmd.Printf("\nView the Web UI: %s\n", accessURLParsed.String())
@ -630,6 +698,11 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
defer client.HTTPClient.CloseIdleConnections() defer client.HTTPClient.CloseIdleConnections()
} }
// This is helpful for tests, but can be silently ignored.
// Coder may be ran as users that don't have permission to write in the homedir,
// such as via the systemd service.
_ = config.URL().Write(client.URL.String())
// Since errCh only has one buffered slot, all routines // Since errCh only has one buffered slot, all routines
// sending on it must be wrapped in a select/default to // sending on it must be wrapped in a select/default to
// avoid leaving dangling goroutines waiting for the // avoid leaving dangling goroutines waiting for the
@ -657,40 +730,65 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
shutdownConnsCtx, shutdownConns := context.WithCancel(ctx) shutdownConnsCtx, shutdownConns := context.WithCancel(ctx)
defer shutdownConns() defer shutdownConns()
// ReadHeaderTimeout is purposefully not enabled. It caused some issues with // Wrap the server in middleware that redirects to the access URL if
// websockets over the dev tunnel. // the request is not to a local IP.
var handler http.Handler = coderAPI.RootHandler
if shouldRedirectHTTPToAccessURL {
handler = redirectHTTPToAccessURL(handler, accessURLParsed)
}
// ReadHeaderTimeout is purposefully not enabled. It caused some
// issues with websockets over the dev tunnel.
// See: https://github.com/coder/coder/pull/3730 // See: https://github.com/coder/coder/pull/3730
//nolint:gosec //nolint:gosec
server := &http.Server{ httpServer := &http.Server{
// These errors are typically noise like "TLS: EOF". Vault does similar: // These errors are typically noise like "TLS: EOF". Vault does
// similar:
// https://github.com/hashicorp/vault/blob/e2490059d0711635e529a4efcbaa1b26998d6e1c/command/server.go#L2714 // https://github.com/hashicorp/vault/blob/e2490059d0711635e529a4efcbaa1b26998d6e1c/command/server.go#L2714
ErrorLog: log.New(io.Discard, "", 0), ErrorLog: log.New(io.Discard, "", 0),
Handler: coderAPI.RootHandler, Handler: handler,
BaseContext: func(_ net.Listener) context.Context { BaseContext: func(_ net.Listener) context.Context {
return shutdownConnsCtx return shutdownConnsCtx
}, },
} }
defer func() { defer func() {
_ = shutdownWithTimeout(server.Shutdown, 5*time.Second) _ = shutdownWithTimeout(httpServer.Shutdown, 5*time.Second)
}() }()
eg := errgroup.Group{} // We call this in the routine so we can kill the other listeners if
eg.Go(func() error { // one of them fails.
// Make sure to close the tunnel listener if we exit so the closeListenersNow := func() {
// errgroup doesn't wait forever! if httpListener != nil {
if tunnel != nil { _ = httpListener.Close()
defer tunnel.Listener.Close()
} }
if httpsListener != nil {
_ = httpsListener.Close()
}
if tunnel != nil {
_ = tunnel.Listener.Close()
}
}
return server.Serve(listener) eg := errgroup.Group{}
}) if httpListener != nil {
if tunnel != nil {
eg.Go(func() error { eg.Go(func() error {
defer listener.Close() defer closeListenersNow()
return httpServer.Serve(httpListener)
return server.Serve(tunnel.Listener)
}) })
} }
if httpsListener != nil {
eg.Go(func() error {
defer closeListenersNow()
return httpServer.Serve(httpsListener)
})
}
if tunnel != nil {
eg.Go(func() error {
defer closeListenersNow()
return httpServer.Serve(tunnel.Listener)
})
}
go func() { go func() {
select { select {
case errCh <- eg.Wait(): case errCh <- eg.Wait():
@ -718,11 +816,6 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
autobuildExecutor := executor.New(ctx, options.Database, logger, autobuildPoller.C) autobuildExecutor := executor.New(ctx, options.Database, logger, autobuildPoller.C)
autobuildExecutor.Run() autobuildExecutor.Run()
// This is helpful for tests, but can be silently ignored.
// Coder may be ran as users that don't have permission to write in the homedir,
// such as via the systemd service.
_ = config.URL().Write(client.URL.String())
// Currently there is no way to ask the server to shut // Currently there is no way to ask the server to shut
// itself down, so any exit signal will result in a non-zero // itself down, so any exit signal will result in a non-zero
// exit of the server. // exit of the server.
@ -759,7 +852,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
// in-flight requests, give in-flight requests 5 seconds to // in-flight requests, give in-flight requests 5 seconds to
// complete. // complete.
cmd.Println("Shutting down API server...") cmd.Println("Shutting down API server...")
err = shutdownWithTimeout(server.Shutdown, 3*time.Second) err = shutdownWithTimeout(httpServer.Shutdown, 3*time.Second)
if err != nil { if err != nil {
cmd.Printf("API server shutdown took longer than 3s: %s\n", err) cmd.Printf("API server shutdown took longer than 3s: %s\n", err)
} else { } else {
@ -1357,3 +1450,14 @@ func configureHTTPClient(ctx context.Context, clientCertFile, clientKeyFile stri
} }
return ctx, &http.Client{}, nil return ctx, &http.Client{}, nil
} }
func redirectHTTPToAccessURL(handler http.Handler, accessURL *url.URL) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.TLS == nil {
http.Redirect(w, r, accessURL.String(), http.StatusTemporaryRedirect)
return
}
handler.ServeHTTP(w, r)
})
}

View File

@ -55,7 +55,7 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t, root, cfg := clitest.New(t,
"server", "server",
"--address", ":0", "--http-address", ":0",
"--access-url", "http://example.com", "--access-url", "http://example.com",
"--postgres-url", connectionURL, "--postgres-url", connectionURL,
"--cache-dir", t.TempDir(), "--cache-dir", t.TempDir(),
@ -89,7 +89,7 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t, root, cfg := clitest.New(t,
"server", "server",
"--address", ":0", "--http-address", ":0",
"--access-url", "http://example.com", "--access-url", "http://example.com",
"--cache-dir", t.TempDir(), "--cache-dir", t.TempDir(),
) )
@ -129,7 +129,7 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t, root, cfg := clitest.New(t,
"server", "server",
"--in-memory", "--in-memory",
"--address", ":0", "--http-address", ":0",
"--access-url", "http://localhost:3000/", "--access-url", "http://localhost:3000/",
"--cache-dir", t.TempDir(), "--cache-dir", t.TempDir(),
) )
@ -161,7 +161,7 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t, root, cfg := clitest.New(t,
"server", "server",
"--in-memory", "--in-memory",
"--address", ":0", "--http-address", ":0",
"--access-url", "https://foobarbaz.mydomain", "--access-url", "https://foobarbaz.mydomain",
"--cache-dir", t.TempDir(), "--cache-dir", t.TempDir(),
) )
@ -191,7 +191,7 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t, root, cfg := clitest.New(t,
"server", "server",
"--in-memory", "--in-memory",
"--address", ":0", "--http-address", ":0",
"--access-url", "https://google.com", "--access-url", "https://google.com",
"--cache-dir", t.TempDir(), "--cache-dir", t.TempDir(),
) )
@ -220,7 +220,7 @@ func TestServer(t *testing.T) {
root, _ := clitest.New(t, root, _ := clitest.New(t,
"server", "server",
"--in-memory", "--in-memory",
"--address", ":0", "--http-address", ":0",
"--access-url", "google.com", "--access-url", "google.com",
"--cache-dir", t.TempDir(), "--cache-dir", t.TempDir(),
) )
@ -236,9 +236,10 @@ func TestServer(t *testing.T) {
root, _ := clitest.New(t, root, _ := clitest.New(t,
"server", "server",
"--in-memory", "--in-memory",
"--address", ":0", "--http-address", "",
"--access-url", "http://example.com", "--access-url", "http://example.com",
"--tls-enable", "--tls-enable",
"--tls-address", ":0",
"--tls-min-version", "tls9", "--tls-min-version", "tls9",
"--cache-dir", t.TempDir(), "--cache-dir", t.TempDir(),
) )
@ -253,9 +254,10 @@ func TestServer(t *testing.T) {
root, _ := clitest.New(t, root, _ := clitest.New(t,
"server", "server",
"--in-memory", "--in-memory",
"--address", ":0", "--http-address", "",
"--access-url", "http://example.com", "--access-url", "http://example.com",
"--tls-enable", "--tls-enable",
"--tls-address", ":0",
"--tls-client-auth", "something", "--tls-client-auth", "something",
"--cache-dir", t.TempDir(), "--cache-dir", t.TempDir(),
) )
@ -310,7 +312,7 @@ func TestServer(t *testing.T) {
args := []string{ args := []string{
"server", "server",
"--in-memory", "--in-memory",
"--address", ":0", "--http-address", ":0",
"--access-url", "http://example.com", "--access-url", "http://example.com",
"--cache-dir", t.TempDir(), "--cache-dir", t.TempDir(),
} }
@ -331,9 +333,10 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t, root, cfg := clitest.New(t,
"server", "server",
"--in-memory", "--in-memory",
"--address", ":0", "--http-address", "",
"--access-url", "http://example.com", "--access-url", "http://example.com",
"--tls-enable", "--tls-enable",
"--tls-address", ":0",
"--tls-cert-file", certPath, "--tls-cert-file", certPath,
"--tls-key-file", keyPath, "--tls-key-file", keyPath,
"--cache-dir", t.TempDir(), "--cache-dir", t.TempDir(),
@ -371,9 +374,10 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t, root, cfg := clitest.New(t,
"server", "server",
"--in-memory", "--in-memory",
"--address", ":0", "--http-address", "",
"--access-url", "http://example.com", "--access-url", "http://example.com",
"--tls-enable", "--tls-enable",
"--tls-address", ":0",
"--tls-cert-file", cert1Path, "--tls-cert-file", cert1Path,
"--tls-key-file", key1Path, "--tls-key-file", key1Path,
"--tls-cert-file", cert2Path, "--tls-cert-file", cert2Path,
@ -443,6 +447,334 @@ func TestServer(t *testing.T) {
cancelFunc() cancelFunc()
require.NoError(t, <-errC) require.NoError(t, <-errC)
}) })
t.Run("TLSAndHTTP", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
certPath, keyPath := generateTLSCertificate(t)
root, _ := clitest.New(t,
"server",
"--in-memory",
"--http-address", ":0",
"--access-url", "https://example.com",
"--tls-enable",
"--tls-redirect-http-to-https=false",
"--tls-address", ":0",
"--tls-cert-file", certPath,
"--tls-key-file", keyPath,
"--cache-dir", t.TempDir(),
)
pty := ptytest.New(t)
root.SetOutput(pty.Output())
root.SetErr(pty.Output())
errC := make(chan error, 1)
go func() {
errC <- root.ExecuteContext(ctx)
}()
// We can't use waitAccessURL as it will only return the HTTP URL.
const httpLinePrefix = "Started HTTP listener at "
pty.ExpectMatch(httpLinePrefix)
httpLine := pty.ReadLine()
httpAddr := strings.TrimSpace(strings.TrimPrefix(httpLine, httpLinePrefix))
require.NotEmpty(t, httpAddr)
const tlsLinePrefix = "Started TLS/HTTPS listener at "
pty.ExpectMatch(tlsLinePrefix)
tlsLine := pty.ReadLine()
tlsAddr := strings.TrimSpace(strings.TrimPrefix(tlsLine, tlsLinePrefix))
require.NotEmpty(t, tlsAddr)
// Verify HTTP
httpURL, err := url.Parse(httpAddr)
require.NoError(t, err)
client := codersdk.New(httpURL)
client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
_, err = client.HasFirstUser(ctx)
require.NoError(t, err)
// Verify TLS
tlsURL, err := url.Parse(tlsAddr)
require.NoError(t, err)
client = codersdk.New(tlsURL)
client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
client.HTTPClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
//nolint:gosec
InsecureSkipVerify: true,
},
},
}
_, err = client.HasFirstUser(ctx)
require.NoError(t, err)
cancelFunc()
require.NoError(t, <-errC)
})
t.Run("TLSRedirect", func(t *testing.T) {
t.Parallel()
cases := []struct {
name string
httpListener bool
tlsListener bool
accessURL string
// Empty string means no redirect.
expectRedirect string
}{
{
name: "OK",
httpListener: true,
tlsListener: true,
accessURL: "https://example.com",
expectRedirect: "https://example.com",
},
{
name: "NoTLSListener",
httpListener: true,
tlsListener: false,
accessURL: "https://example.com",
expectRedirect: "",
},
{
name: "NoHTTPListener",
httpListener: false,
tlsListener: true,
accessURL: "https://example.com",
expectRedirect: "",
},
}
for _, c := range cases {
c := c
t.Run(c.name, func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
certPath, keyPath := generateTLSCertificate(t)
flags := []string{
"server",
"--in-memory",
"--cache-dir", t.TempDir(),
}
if c.httpListener {
flags = append(flags, "--http-address", ":0")
}
if c.tlsListener {
flags = append(flags,
"--tls-enable",
"--tls-address", ":0",
"--tls-cert-file", certPath,
"--tls-key-file", keyPath,
)
}
if c.accessURL != "" {
flags = append(flags, "--access-url", c.accessURL)
}
root, _ := clitest.New(t, flags...)
pty := ptytest.New(t)
root.SetOutput(pty.Output())
root.SetErr(pty.Output())
errC := make(chan error, 1)
go func() {
errC <- root.ExecuteContext(ctx)
}()
var (
httpAddr string
tlsAddr string
)
// We can't use waitAccessURL as it will only return the HTTP URL.
if c.httpListener {
const httpLinePrefix = "Started HTTP listener at "
pty.ExpectMatch(httpLinePrefix)
httpLine := pty.ReadLine()
httpAddr = strings.TrimSpace(strings.TrimPrefix(httpLine, httpLinePrefix))
require.NotEmpty(t, httpAddr)
}
if c.tlsListener {
const tlsLinePrefix = "Started TLS/HTTPS listener at "
pty.ExpectMatch(tlsLinePrefix)
tlsLine := pty.ReadLine()
tlsAddr = strings.TrimSpace(strings.TrimPrefix(tlsLine, tlsLinePrefix))
require.NotEmpty(t, tlsAddr)
}
// Verify HTTP redirects (or not)
if c.httpListener {
httpURL, err := url.Parse(httpAddr)
require.NoError(t, err)
client := codersdk.New(httpURL)
client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
resp, err := client.Request(ctx, http.MethodGet, "/api/v2/buildinfo", nil)
require.NoError(t, err)
defer resp.Body.Close()
if c.expectRedirect == "" {
require.Equal(t, http.StatusOK, resp.StatusCode)
} else {
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
require.Equal(t, c.expectRedirect, resp.Header.Get("Location"))
}
}
// Verify TLS
if c.tlsListener {
tlsURL, err := url.Parse(tlsAddr)
require.NoError(t, err)
client := codersdk.New(tlsURL)
client.HTTPClient = &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
//nolint:gosec
InsecureSkipVerify: true,
},
},
}
_, err = client.HasFirstUser(ctx)
require.NoError(t, err)
cancelFunc()
require.NoError(t, <-errC)
}
})
}
})
t.Run("NoAddress", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
root, _ := clitest.New(t,
"server",
"--in-memory",
"--http-address", "",
"--tls-enable=false",
"--tls-address", "",
)
err := root.ExecuteContext(ctx)
require.Error(t, err)
require.ErrorContains(t, err, "either HTTP or TLS must be enabled")
})
t.Run("NoTLSAddress", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
root, _ := clitest.New(t,
"server",
"--in-memory",
"--tls-enable=true",
"--tls-address", "",
)
err := root.ExecuteContext(ctx)
require.Error(t, err)
require.ErrorContains(t, err, "TLS address must be set if TLS is enabled")
})
// DeprecatedAddress is a test for the deprecated --address flag. If
// specified, --http-address and --tls-address are both ignored, a warning
// is printed, and the server will either be HTTP-only or TLS-only depending
// on if --tls-enable is set.
t.Run("DeprecatedAddress", func(t *testing.T) {
t.Parallel()
t.Run("HTTP", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
root, cfg := clitest.New(t,
"server",
"--in-memory",
"--address", ":0",
"--access-url", "http://example.com",
"--cache-dir", t.TempDir(),
)
pty := ptytest.New(t)
root.SetOutput(pty.Output())
root.SetErr(pty.Output())
errC := make(chan error, 1)
go func() {
errC <- root.ExecuteContext(ctx)
}()
pty.ExpectMatch("--address and -a are deprecated")
accessURL := waitAccessURL(t, cfg)
require.Equal(t, "http", accessURL.Scheme)
client := codersdk.New(accessURL)
_, err := client.HasFirstUser(ctx)
require.NoError(t, err)
cancelFunc()
require.NoError(t, <-errC)
})
t.Run("TLS", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
certPath, keyPath := generateTLSCertificate(t)
root, cfg := clitest.New(t,
"server",
"--in-memory",
"--address", ":0",
"--access-url", "http://example.com",
"--tls-enable",
"--tls-cert-file", certPath,
"--tls-key-file", keyPath,
"--cache-dir", t.TempDir(),
)
pty := ptytest.New(t)
root.SetOutput(pty.Output())
root.SetErr(pty.Output())
errC := make(chan error, 1)
go func() {
errC <- root.ExecuteContext(ctx)
}()
pty.ExpectMatch("--address and -a are deprecated")
accessURL := waitAccessURL(t, cfg)
require.Equal(t, "https", accessURL.Scheme)
client := codersdk.New(accessURL)
client.HTTPClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
//nolint:gosec
InsecureSkipVerify: true,
},
},
}
_, err := client.HasFirstUser(ctx)
require.NoError(t, err)
cancelFunc()
require.NoError(t, <-errC)
})
})
// This cannot be ran in parallel because it uses a signal. // This cannot be ran in parallel because it uses a signal.
//nolint:paralleltest //nolint:paralleltest
t.Run("Shutdown", func(t *testing.T) { t.Run("Shutdown", func(t *testing.T) {
@ -456,7 +788,7 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t, root, cfg := clitest.New(t,
"server", "server",
"--in-memory", "--in-memory",
"--address", ":0", "--http-address", ":0",
"--access-url", "http://example.com", "--access-url", "http://example.com",
"--provisioner-daemons", "1", "--provisioner-daemons", "1",
"--cache-dir", t.TempDir(), "--cache-dir", t.TempDir(),
@ -483,7 +815,7 @@ func TestServer(t *testing.T) {
root, _ := clitest.New(t, root, _ := clitest.New(t,
"server", "server",
"--in-memory", "--in-memory",
"--address", ":0", "--http-address", ":0",
"--access-url", "http://example.com", "--access-url", "http://example.com",
"--trace=true", "--trace=true",
"--cache-dir", t.TempDir(), "--cache-dir", t.TempDir(),
@ -521,7 +853,7 @@ func TestServer(t *testing.T) {
root, _ := clitest.New(t, root, _ := clitest.New(t,
"server", "server",
"--in-memory", "--in-memory",
"--address", ":0", "--http-address", ":0",
"--access-url", "http://example.com", "--access-url", "http://example.com",
"--telemetry", "--telemetry",
"--telemetry-url", server.URL, "--telemetry-url", server.URL,
@ -552,7 +884,7 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t, root, cfg := clitest.New(t,
"server", "server",
"--in-memory", "--in-memory",
"--address", ":0", "--http-address", ":0",
"--access-url", "http://example.com", "--access-url", "http://example.com",
"--provisioner-daemons", "1", "--provisioner-daemons", "1",
"--prometheus-enable", "--prometheus-enable",
@ -605,7 +937,7 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t, root, cfg := clitest.New(t,
"server", "server",
"--in-memory", "--in-memory",
"--address", ":0", "--http-address", ":0",
"--access-url", "http://example.com", "--access-url", "http://example.com",
"--oauth2-github-allow-everyone", "--oauth2-github-allow-everyone",
"--oauth2-github-client-id", "fake", "--oauth2-github-client-id", "fake",
@ -646,7 +978,7 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t, root, cfg := clitest.New(t,
"server", "server",
"--in-memory", "--in-memory",
"--address", ":0", "--http-address", ":0",
"--access-url", "http://example.com", "--access-url", "http://example.com",
) )
serverErr := make(chan error, 1) serverErr := make(chan error, 1)
@ -674,7 +1006,7 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t, root, cfg := clitest.New(t,
"server", "server",
"--in-memory", "--in-memory",
"--address", ":0", "--http-address", ":0",
"--access-url", "http://example.com", "--access-url", "http://example.com",
"--api-rate-limit", val, "--api-rate-limit", val,
) )
@ -702,7 +1034,7 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t, root, cfg := clitest.New(t,
"server", "server",
"--in-memory", "--in-memory",
"--address", ":0", "--http-address", ":0",
"--access-url", "http://example.com", "--access-url", "http://example.com",
"--api-rate-limit", "-1", "--api-rate-limit", "-1",
) )

View File

@ -14,9 +14,6 @@ Flags:
This must be accessible by all This must be accessible by all
provisioned workspaces. provisioned workspaces.
Consumes $CODER_ACCESS_URL Consumes $CODER_ACCESS_URL
-a, --address string Bind address of the server.
Consumes $CODER_ADDRESS (default
"127.0.0.1:3000")
--api-rate-limit int Maximum number of requests per minute --api-rate-limit int Maximum number of requests per minute
allowed to the API per user, or per IP allowed to the API per user, or per IP
address for unauthenticated users. address for unauthenticated users.
@ -65,6 +62,10 @@ Flags:
production. production.
Consumes $CODER_EXPERIMENTAL Consumes $CODER_EXPERIMENTAL
-h, --help help for server -h, --help help for server
--http-address string HTTP bind address of the server. Unset to
disable the HTTP endpoint.
Consumes $CODER_HTTP_ADDRESS (default
"127.0.0.1:3000")
--max-token-lifetime duration The maximum lifetime duration for any --max-token-lifetime duration The maximum lifetime duration for any
user creating a token. user creating a token.
Consumes $CODER_MAX_TOKEN_LIFETIME Consumes $CODER_MAX_TOKEN_LIFETIME
@ -178,6 +179,9 @@ Flags:
product. Disabling telemetry also product. Disabling telemetry also
disables this option. disables this option.
Consumes $CODER_TELEMETRY_TRACE Consumes $CODER_TELEMETRY_TRACE
--tls-address string HTTPS bind address of the server.
Consumes $CODER_TLS_ADDRESS (default
"127.0.0.1:3443")
--tls-cert-file strings Path to each certificate for TLS. It --tls-cert-file strings Path to each certificate for TLS. It
requires a PEM-encoded file. To configure requires a PEM-encoded file. To configure
the listener to use a CA certificate, the listener to use a CA certificate,
@ -216,6 +220,13 @@ Flags:
"tls12" or "tls13" "tls12" or "tls13"
Consumes $CODER_TLS_MIN_VERSION (default Consumes $CODER_TLS_MIN_VERSION (default
"tls12") "tls12")
--tls-redirect-http-to-https Whether HTTP requests will be redirected
to the access URL (if it's a https URL
and TLS is enabled). Requests to local IP
addresses are never redirected regardless
of this setting.
Consumes $CODER_TLS_REDIRECT_HTTP
(default true)
--trace Whether application tracing data is --trace Whether application tracing data is
collected. It exports to a backend collected. It exports to a backend
configured by environment variables. See: configured by environment variables. See:

View File

@ -11,9 +11,11 @@ import (
// DeploymentConfig is the central configuration for the coder server. // DeploymentConfig is the central configuration for the coder server.
type DeploymentConfig struct { type DeploymentConfig struct {
AccessURL *DeploymentConfigField[string] `json:"access_url" typescript:",notnull"` AccessURL *DeploymentConfigField[string] `json:"access_url" typescript:",notnull"`
WildcardAccessURL *DeploymentConfigField[string] `json:"wildcard_access_url" typescript:",notnull"` WildcardAccessURL *DeploymentConfigField[string] `json:"wildcard_access_url" typescript:",notnull"`
// DEPRECATED: Use HTTPAddress or TLS.Address instead.
Address *DeploymentConfigField[string] `json:"address" typescript:",notnull"` Address *DeploymentConfigField[string] `json:"address" typescript:",notnull"`
HTTPAddress *DeploymentConfigField[string] `json:"http_address" typescript:",notnull"`
AutobuildPollInterval *DeploymentConfigField[time.Duration] `json:"autobuild_poll_interval" typescript:",notnull"` AutobuildPollInterval *DeploymentConfigField[time.Duration] `json:"autobuild_poll_interval" typescript:",notnull"`
DERP *DERP `json:"derp" typescript:",notnull"` DERP *DERP `json:"derp" typescript:",notnull"`
GitAuth *DeploymentConfigField[[]GitAuthConfig] `json:"gitauth" typescript:",notnull"` GitAuth *DeploymentConfigField[[]GitAuthConfig] `json:"gitauth" typescript:",notnull"`
@ -106,6 +108,8 @@ type TelemetryConfig struct {
type TLSConfig struct { type TLSConfig struct {
Enable *DeploymentConfigField[bool] `json:"enable" typescript:",notnull"` Enable *DeploymentConfigField[bool] `json:"enable" typescript:",notnull"`
Address *DeploymentConfigField[string] `json:"address" typescript:",notnull"`
RedirectHTTP *DeploymentConfigField[bool] `json:"redirect_http" typescript:",notnull"`
CertFiles *DeploymentConfigField[[]string] `json:"cert_file" typescript:",notnull"` CertFiles *DeploymentConfigField[[]string] `json:"cert_file" typescript:",notnull"`
ClientAuth *DeploymentConfigField[string] `json:"client_auth" typescript:",notnull"` ClientAuth *DeploymentConfigField[string] `json:"client_auth" typescript:",notnull"`
ClientCAFile *DeploymentConfigField[string] `json:"client_ca_file" typescript:",notnull"` ClientCAFile *DeploymentConfigField[string] `json:"client_ca_file" typescript:",notnull"`

View File

@ -43,43 +43,42 @@ Coder Docker image URI
{{- end }} {{- end }}
{{/* {{/*
Coder listen port (must be > 1024) Coder TLS enabled.
*/}} */}}
{{- define "coder.port" }} {{- define "coder.tlsEnabled" -}}
{{- if .Values.coder.tls.secretNames -}} {{- if .Values.coder.tls.secretNames -}}
8443 true
{{- else -}} {{- else -}}
8080 false
{{- end -}} {{- end -}}
{{- end }} {{- end }}
{{/* {{/*
Coder service port Coder TLS environment variables.
*/}} */}}
{{- define "coder.servicePort" }} {{- define "coder.tlsEnv" }}
{{- if .Values.coder.tls.secretNames -}} {{- if eq (include "coder.tlsEnabled" .) "true" }}
443 - name: CODER_TLS_ENABLE
{{- else -}} value: "true"
80 - name: CODER_TLS_ADDRESS
{{- end -}} value: "0.0.0.0:8443"
- name: CODER_TLS_CERT_FILE
value: "{{ range $idx, $secretName := .Values.coder.tls.secretNames -}}{{ if $idx }},{{ end }}/etc/ssl/certs/coder/{{ $secretName }}/tls.crt{{- end }}"
- name: CODER_TLS_KEY_FILE
value: "{{ range $idx, $secretName := .Values.coder.tls.secretNames -}}{{ if $idx }},{{ end }}/etc/ssl/certs/coder/{{ $secretName }}/tls.key{{- end }}"
{{- end }}
{{- end }} {{- end }}
{{/* {{/*
Port name Coder default access URL
*/}} */}}
{{- define "coder.portName" }} {{- define "coder.defaultAccessURL" }}
{{- if .Values.coder.tls.secretNames -}} {{- if eq (include "coder.tlsEnabled" .) "true" -}}
https https
{{- else -}} {{- else -}}
http http
{{- end -}} {{- end -}}
{{- end }} ://coder.{{ .Release.Namespace }}.svc.cluster.local
{{/*
Scheme
*/}}
{{- define "coder.scheme" }}
{{- include "coder.portName" . | upper -}}
{{- end }} {{- end }}
{{/* {{/*
@ -139,20 +138,6 @@ volumeMounts: []
{{- end -}} {{- end -}}
{{- end }} {{- end }}
{{/*
Coder TLS environment variables.
*/}}
{{- define "coder.tlsEnv" }}
{{- if .Values.coder.tls.secretNames }}
- name: CODER_TLS_ENABLE
value: "true"
- name: CODER_TLS_CERT_FILE
value: "{{ range $idx, $secretName := .Values.coder.tls.secretNames -}}{{ if $idx }},{{ end }}/etc/ssl/certs/coder/{{ $secretName }}/tls.crt{{- end }}"
- name: CODER_TLS_KEY_FILE
value: "{{ range $idx, $secretName := .Values.coder.tls.secretNames -}}{{ if $idx }},{{ end }}/etc/ssl/certs/coder/{{ $secretName }}/tls.key{{- end }}"
{{- end }}
{{- end }}
{{/* {{/*
Coder ingress wildcard hostname with the wildcard suffix stripped. Coder ingress wildcard hostname with the wildcard suffix stripped.
*/}} */}}

View File

@ -52,8 +52,10 @@ spec:
resources: resources:
{{- toYaml .Values.coder.resources | nindent 12 }} {{- toYaml .Values.coder.resources | nindent 12 }}
env: env:
- name: CODER_ADDRESS - name: CODER_HTTP_ADDRESS
value: "0.0.0.0:{{ include "coder.port" . }}" value: "0.0.0.0:8080"
- name: CODER_PROMETHEUS_ADDRESS
value: "0.0.0.0:6060"
# Set the default access URL so a `helm apply` works by default. # Set the default access URL so a `helm apply` works by default.
# See: https://github.com/coder/coder/issues/5024 # See: https://github.com/coder/coder/issues/5024
{{- $hasAccessURL := false }} {{- $hasAccessURL := false }}
@ -64,7 +66,7 @@ spec:
{{- end }} {{- end }}
{{- if not $hasAccessURL }} {{- if not $hasAccessURL }}
- name: CODER_ACCESS_URL - name: CODER_ACCESS_URL
value: "{{ include "coder.portName" . }}://coder.{{.Release.Namespace}}.svc.cluster.local" value: {{ include "coder.defaultAccessURL" . | quote }}
{{- end }} {{- end }}
# Used for inter-pod communication with high-availability. # Used for inter-pod communication with high-availability.
- name: KUBE_POD_IP - name: KUBE_POD_IP
@ -72,38 +74,37 @@ spec:
fieldRef: fieldRef:
fieldPath: status.podIP fieldPath: status.podIP
- name: CODER_DERP_SERVER_RELAY_URL - name: CODER_DERP_SERVER_RELAY_URL
value: "{{ include "coder.portName" . }}://$(KUBE_POD_IP):{{ include "coder.port" . }}" value: "http://$(KUBE_POD_IP):8080"
{{- include "coder.tlsEnv" . | nindent 12 }} {{- include "coder.tlsEnv" . | nindent 12 }}
{{- with .Values.coder.env -}} {{- with .Values.coder.env -}}
{{ toYaml . | nindent 12 }} {{ toYaml . | nindent 12 }}
{{- end }} {{- end }}
{{- range .Values.coder.env }}
{{- if eq .name "CODER_PROMETHEUS_ADDRESS" }}
- name: CODER_PROMETHEUS_ENABLE
value: "true"
{{- end }}
{{- end }}
ports: ports:
- name: {{ include "coder.portName" . | quote }} - name: "http"
containerPort: {{ include "coder.port" . }} containerPort: 8080
protocol: TCP protocol: TCP
{{- if eq (include "coder.tlsEnabled" .) "true" }}
- name: "https"
containerPort: 8443
protocol: TCP
{{- end }}
{{- range .Values.coder.env }} {{- range .Values.coder.env }}
{{- if eq .name "CODER_PROMETHEUS_ADDRESS" }} {{- if and (eq .name "CODER_PROMETHEUS_ENABLE") (eq .value "true") }}
- name: "prometheus-http" - name: "prometheus-http"
containerPort: {{ (splitList ":" .value) | last }} containerPort: 6060
protocol: TCP protocol: TCP
{{- end }} {{- end }}
{{- end }} {{- end }}
readinessProbe: readinessProbe:
httpGet: httpGet:
path: /api/v2/buildinfo path: /api/v2/buildinfo
port: {{ include "coder.portName" . | quote }} port: "http"
scheme: {{ include "coder.scheme" . | quote }} scheme: "HTTP"
livenessProbe: livenessProbe:
httpGet: httpGet:
path: /api/v2/buildinfo path: /api/v2/buildinfo
port: {{ include "coder.portName" . | quote }} port: "http"
scheme: {{ include "coder.scheme" . | quote }} scheme: "HTTP"
{{- include "coder.volumeMounts" . | nindent 10 }} {{- include "coder.volumeMounts" . | nindent 10 }}
{{- include "coder.volumes" . | nindent 6 }} {{- include "coder.volumes" . | nindent 6 }}

View File

@ -25,7 +25,7 @@ spec:
service: service:
name: coder name: coder
port: port:
name: {{ include "coder.portName" . | quote }} name: "http"
{{- if .Values.coder.ingress.wildcardHost }} {{- if .Values.coder.ingress.wildcardHost }}
- host: {{ include "coder.ingressWildcardHost" . | quote }} - host: {{ include "coder.ingressWildcardHost" . | quote }}
@ -37,7 +37,7 @@ spec:
service: service:
name: coder name: coder
port: port:
name: {{ include "coder.portName" . | quote }} name: "http"
{{- end }} {{- end }}
{{- if .Values.coder.ingress.tls.enable }} {{- if .Values.coder.ingress.tls.enable }}

View File

@ -12,10 +12,16 @@ spec:
type: {{ .Values.coder.service.type }} type: {{ .Values.coder.service.type }}
sessionAffinity: ClientIP sessionAffinity: ClientIP
ports: ports:
- name: {{ include "coder.portName" . | quote }} - name: "http"
port: {{ include "coder.servicePort" . }} port: 80
targetPort: {{ include "coder.portName" . | quote }} targetPort: "http"
protocol: TCP protocol: TCP
{{- if eq (include "coder.tlsEnabled" .) "true" }}
- name: "https"
port: 443
targetPort: "https"
protocol: TCP
{{- end }}
{{- if eq "LoadBalancer" .Values.coder.service.type }} {{- if eq "LoadBalancer" .Values.coder.service.type }}
{{- with .Values.coder.service.loadBalancerIP }} {{- with .Values.coder.service.loadBalancerIP }}
loadBalancerIP: {{ . | quote }} loadBalancerIP: {{ . | quote }}

View File

@ -45,11 +45,13 @@ coder:
# for information about what environment variables can be set. # for information about what environment variables can be set.
# Note: The following environment variables are set by default and cannot be # Note: The following environment variables are set by default and cannot be
# overridden: # overridden:
# - CODER_ADDRESS: set to 0.0.0.0:80 and cannot be changed. # - CODER_HTTP_ADDRESS: set to 0.0.0.0:8080 and cannot be changed.
# - CODER_TLS_ADDRESS: set to 0.0.0.0:8443 if tls.secretName is not empty.
# - CODER_TLS_ENABLE: set if tls.secretName is not empty. # - CODER_TLS_ENABLE: set if tls.secretName is not empty.
# - CODER_TLS_CERT_FILE: set if tls.secretName is not empty. # - CODER_TLS_CERT_FILE: set if tls.secretName is not empty.
# - CODER_TLS_KEY_FILE: set if tls.secretName is not empty. # - CODER_TLS_KEY_FILE: set if tls.secretName is not empty.
# - CODER_PROMETHEUS_ENABLE: set if CODER_PROMETHEUS_ADDRESS is not empty. # - CODER_PROMETHEUS_ADDRESS: set to 0.0.0.0:6060 and cannot be changed.
# Prometheus must still be enabled by setting CODER_PROMETHEUS_ENABLE.
env: [] env: []
# coder.tls -- The TLS configuration for Coder. # coder.tls -- The TLS configuration for Coder.

View File

@ -182,6 +182,74 @@ func (p *PTY) ExpectMatch(str string) string {
} }
} }
func (p *PTY) ReadLine() string {
p.t.Helper()
// timeout, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
timeout, cancel := context.WithCancel(context.Background())
defer cancel()
var buffer bytes.Buffer
match := make(chan error, 1)
go func() {
defer close(match)
match <- func() error {
for {
r, _, err := p.runeReader.ReadRune()
if err != nil {
return err
}
if r == '\n' {
return nil
}
if r == '\r' {
// Peek the next rune to see if it's an LF and then consume
// it.
// Unicode code points can be up to 4 bytes, but the
// ones we're looking for are only 1 byte.
b, _ := p.runeReader.Peek(1)
if len(b) == 0 {
return nil
}
r, _ = utf8.DecodeRune(b)
if r == '\n' {
_, _, err = p.runeReader.ReadRune()
if err != nil {
return err
}
}
return nil
}
_, err = buffer.WriteRune(r)
if err != nil {
return err
}
}
}()
}()
select {
case err := <-match:
if err != nil {
p.fatalf("read error", "%v (wanted newline; got %q)", err, buffer.String())
return ""
}
p.logf("matched newline = %q", buffer.String())
return buffer.String()
case <-timeout.Done():
// Ensure goroutine is cleaned up before test exit.
_ = p.close("expect match timeout")
<-match
p.fatalf("match exceeded deadline", "wanted newline; got %q", buffer.String())
return ""
}
}
func (p *PTY) Write(r rune) { func (p *PTY) Write(r rune) {
p.t.Helper() p.t.Helper()

View File

@ -2,6 +2,7 @@ package ptytest_test
import ( import (
"fmt" "fmt"
"runtime"
"strings" "strings"
"testing" "testing"
@ -21,6 +22,23 @@ func TestPtytest(t *testing.T) {
pty.WriteLine("read") pty.WriteLine("read")
}) })
t.Run("ReadLine", func(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("ReadLine is glitchy on windows when it comes to the final line of output it seems")
}
pty := ptytest.New(t)
// The PTY expands these to \r\n (even on linux).
pty.Output().Write([]byte("line 1\nline 2\nline 3\nline 4\nline 5"))
require.Equal(t, "line 1", pty.ReadLine())
require.Equal(t, "line 2", pty.ReadLine())
require.Equal(t, "line 3", pty.ReadLine())
require.Equal(t, "line 4", pty.ReadLine())
require.Equal(t, "line 5", pty.ExpectMatch("5"))
})
// See https://github.com/coder/coder/issues/2122 for the motivation // See https://github.com/coder/coder/issues/2122 for the motivation
// behind this test. // behind this test.
t.Run("Cobra ptytest should not hang when output is not consumed", func(t *testing.T) { t.Run("Cobra ptytest should not hang when output is not consumed", func(t *testing.T) {

View File

@ -121,7 +121,7 @@ fatal() {
trap 'fatal "Script encountered an error"' ERR trap 'fatal "Script encountered an error"' ERR
cdroot cdroot
start_cmd API "" "${CODER_DEV_SHIM}" server --address 0.0.0.0:3000 start_cmd API "" "${CODER_DEV_SHIM}" server --http-address 0.0.0.0:3000
echo '== Waiting for Coder to become ready' echo '== Waiting for Coder to become ready'
# Start the timeout in the background so interrupting this script # Start the timeout in the background so interrupting this script

View File

@ -277,6 +277,7 @@ export interface DeploymentConfig {
readonly access_url: DeploymentConfigField<string> readonly access_url: DeploymentConfigField<string>
readonly wildcard_access_url: DeploymentConfigField<string> readonly wildcard_access_url: DeploymentConfigField<string>
readonly address: DeploymentConfigField<string> readonly address: DeploymentConfigField<string>
readonly http_address: DeploymentConfigField<string>
readonly autobuild_poll_interval: DeploymentConfigField<number> readonly autobuild_poll_interval: DeploymentConfigField<number>
readonly derp: DERP readonly derp: DERP
readonly gitauth: DeploymentConfigField<GitAuthConfig[]> readonly gitauth: DeploymentConfigField<GitAuthConfig[]>
@ -618,6 +619,8 @@ export interface ServiceBanner {
// From codersdk/deploymentconfig.go // From codersdk/deploymentconfig.go
export interface TLSConfig { export interface TLSConfig {
readonly enable: DeploymentConfigField<boolean> readonly enable: DeploymentConfigField<boolean>
readonly address: DeploymentConfigField<string>
readonly redirect_http: DeploymentConfigField<boolean>
readonly cert_file: DeploymentConfigField<string[]> readonly cert_file: DeploymentConfigField<string[]>
readonly client_auth: DeploymentConfigField<string> readonly client_auth: DeploymentConfigField<string>
readonly client_ca_file: DeploymentConfigField<string> readonly client_ca_file: DeploymentConfigField<string>