Skip to content

Commit 47c2f16

Browse files
Merge branch 'coder:main' into feat/coder-login-secret
2 parents 4222976 + 15157c1 commit 47c2f16

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1081
-121
lines changed

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,8 @@ endef
200200
# calling this manually.
201201
$(CODER_ALL_BINARIES): go.mod go.sum \
202202
$(GO_SRC_FILES) \
203-
$(shell find ./examples/templates)
203+
$(shell find ./examples/templates) \
204+
site/static/error.html
204205

205206
$(get-mode-os-arch-ext)
206207
if [[ "$$os" != "windows" ]] && [[ "$$ext" != "" ]]; then

agent/agent.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -240,10 +240,11 @@ type agent struct {
240240
sshServer *agentssh.Server
241241
sshMaxTimeout time.Duration
242242

243-
lifecycleUpdate chan struct{}
244-
lifecycleReported chan codersdk.WorkspaceAgentLifecycle
245-
lifecycleMu sync.RWMutex // Protects following.
246-
lifecycleStates []agentsdk.PostLifecycleRequest
243+
lifecycleUpdate chan struct{}
244+
lifecycleReported chan codersdk.WorkspaceAgentLifecycle
245+
lifecycleMu sync.RWMutex // Protects following.
246+
lifecycleStates []agentsdk.PostLifecycleRequest
247+
lifecycleLastReportedIndex int // Keeps track of the last lifecycle state we successfully reported.
247248

248249
network *tailnet.Conn
249250
addresses []netip.Prefix
@@ -625,7 +626,6 @@ func (a *agent) reportMetadata(ctx context.Context, conn drpc.Conn) error {
625626
// changes are reported in order.
626627
func (a *agent) reportLifecycle(ctx context.Context, conn drpc.Conn) error {
627628
aAPI := proto.NewDRPCAgentClient(conn)
628-
lastReportedIndex := 0 // Start off with the created state without reporting it.
629629
for {
630630
select {
631631
case <-a.lifecycleUpdate:
@@ -636,20 +636,20 @@ func (a *agent) reportLifecycle(ctx context.Context, conn drpc.Conn) error {
636636
for {
637637
a.lifecycleMu.RLock()
638638
lastIndex := len(a.lifecycleStates) - 1
639-
report := a.lifecycleStates[lastReportedIndex]
640-
if len(a.lifecycleStates) > lastReportedIndex+1 {
641-
report = a.lifecycleStates[lastReportedIndex+1]
639+
report := a.lifecycleStates[a.lifecycleLastReportedIndex]
640+
if len(a.lifecycleStates) > a.lifecycleLastReportedIndex+1 {
641+
report = a.lifecycleStates[a.lifecycleLastReportedIndex+1]
642642
}
643643
a.lifecycleMu.RUnlock()
644644

645-
if lastIndex == lastReportedIndex {
645+
if lastIndex == a.lifecycleLastReportedIndex {
646646
break
647647
}
648648
l, err := agentsdk.ProtoFromLifecycle(report)
649649
if err != nil {
650650
a.logger.Critical(ctx, "failed to convert lifecycle state", slog.F("report", report))
651651
// Skip this report; there is no point retrying. Maybe we can successfully convert the next one?
652-
lastReportedIndex++
652+
a.lifecycleLastReportedIndex++
653653
continue
654654
}
655655
payload := &proto.UpdateLifecycleRequest{Lifecycle: l}
@@ -662,13 +662,13 @@ func (a *agent) reportLifecycle(ctx context.Context, conn drpc.Conn) error {
662662
}
663663

664664
logger.Debug(ctx, "successfully reported lifecycle state")
665-
lastReportedIndex++
665+
a.lifecycleLastReportedIndex++
666666
select {
667667
case a.lifecycleReported <- report.State:
668668
case <-a.lifecycleReported:
669669
a.lifecycleReported <- report.State
670670
}
671-
if lastReportedIndex < lastIndex {
671+
if a.lifecycleLastReportedIndex < lastIndex {
672672
// Keep reporting until we've sent all messages, we can't
673673
// rely on the channel triggering us before the backlog is
674674
// consumed.

cli/exp_scaletest.go

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"strconv"
1515
"strings"
1616
"sync"
17-
"syscall"
1817
"time"
1918

2019
"github.com/google/uuid"
@@ -245,14 +244,8 @@ func (o *scaleTestOutput) write(res harness.Results, stdout io.Writer) error {
245244

246245
// Sync the file to disk if it's a file.
247246
if s, ok := w.(interface{ Sync() error }); ok {
248-
err := s.Sync()
249-
// On Linux, EINVAL is returned when calling fsync on /dev/stdout. We
250-
// can safely ignore this error.
251-
// On macOS, ENOTTY is returned when calling sync on /dev/stdout. We
252-
// can safely ignore this error.
253-
if err != nil && !xerrors.Is(err, syscall.EINVAL) && !xerrors.Is(err, syscall.ENOTTY) {
254-
return xerrors.Errorf("flush output file: %w", err)
255-
}
247+
// Best effort. If we get an error from syncing, just ignore it.
248+
_ = s.Sync()
256249
}
257250

258251
if c != nil {

cli/testdata/coder_server_--help.golden

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ OPTIONS:
6060
--support-links struct[[]codersdk.LinkConfig], $CODER_SUPPORT_LINKS
6161
Support links to display in the top right drop down menu.
6262

63+
--terms-of-service-url string, $CODER_TERMS_OF_SERVICE_URL
64+
A URL to an external Terms of Service that must be accepted by users
65+
when logging in.
66+
6367
--update-check bool, $CODER_UPDATE_CHECK (default: false)
6468
Periodically check for new releases of Coder and inform the owner. The
6569
check is performed once per day.

cli/testdata/server-config.yaml.golden

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,10 @@ inMemoryDatabase: false
414414
# Type of auth to use when connecting to postgres.
415415
# (default: password, type: enum[password\|awsiamrds])
416416
pgAuth: password
417+
# A URL to an external Terms of Service that must be accepted by users when
418+
# logging in.
419+
# (default: <unset>, type: string)
420+
termsOfServiceURL: ""
417421
# The algorithm to use for generating ssh keys. Accepted values are "ed25519",
418422
# "ecdsa", or "rsa4096".
419423
# (default: ed25519, type: string)

coderd/apidoc/docs.go

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/tailnet.go

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ import (
44
"bufio"
55
"context"
66
"crypto/tls"
7+
"errors"
8+
"fmt"
79
"net"
810
"net/http"
911
"net/http/httputil"
1012
"net/netip"
1113
"net/url"
14+
"strings"
1215
"sync"
1316
"sync/atomic"
1417
"time"
@@ -23,6 +26,7 @@ import (
2326
"cdr.dev/slog"
2427
"github.com/coder/coder/v2/coderd/tracing"
2528
"github.com/coder/coder/v2/coderd/workspaceapps"
29+
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
2630
"github.com/coder/coder/v2/codersdk/workspacesdk"
2731
"github.com/coder/coder/v2/site"
2832
"github.com/coder/coder/v2/tailnet"
@@ -341,7 +345,7 @@ type ServerTailnet struct {
341345
totalConns *prometheus.CounterVec
342346
}
343347

344-
func (s *ServerTailnet) ReverseProxy(targetURL, dashboardURL *url.URL, agentID uuid.UUID) *httputil.ReverseProxy {
348+
func (s *ServerTailnet) ReverseProxy(targetURL, dashboardURL *url.URL, agentID uuid.UUID, app appurl.ApplicationURL, wildcardHostname string) *httputil.ReverseProxy {
345349
// Rewrite the targetURL's Host to point to the agent's IP. This is
346350
// necessary because due to TCP connection caching, each agent needs to be
347351
// addressed invidivually. Otherwise, all connections get dialed as
@@ -351,13 +355,46 @@ func (s *ServerTailnet) ReverseProxy(targetURL, dashboardURL *url.URL, agentID u
351355
tgt.Host = net.JoinHostPort(tailnet.IPFromUUID(agentID).String(), port)
352356

353357
proxy := httputil.NewSingleHostReverseProxy(&tgt)
354-
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
358+
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, theErr error) {
359+
var (
360+
desc = "Failed to proxy request to application: " + theErr.Error()
361+
additionalInfo = ""
362+
additionalButtonLink = ""
363+
additionalButtonText = ""
364+
)
365+
366+
var tlsError tls.RecordHeaderError
367+
if (errors.As(theErr, &tlsError) && tlsError.Msg == "first record does not look like a TLS handshake") ||
368+
errors.Is(theErr, http.ErrSchemeMismatch) {
369+
// If the error is due to an HTTP/HTTPS mismatch, we can provide a
370+
// more helpful error message with redirect buttons.
371+
switchURL := url.URL{
372+
Scheme: dashboardURL.Scheme,
373+
}
374+
_, protocol, isPort := app.PortInfo()
375+
if isPort {
376+
targetProtocol := "https"
377+
if protocol == "https" {
378+
targetProtocol = "http"
379+
}
380+
app = app.ChangePortProtocol(targetProtocol)
381+
382+
switchURL.Host = fmt.Sprintf("%s%s", app.String(), strings.TrimPrefix(wildcardHostname, "*"))
383+
additionalButtonLink = switchURL.String()
384+
additionalButtonText = fmt.Sprintf("Switch to %s", strings.ToUpper(targetProtocol))
385+
additionalInfo += fmt.Sprintf("This error seems to be due to an app protocol mismatch, try switching to %s.", strings.ToUpper(targetProtocol))
386+
}
387+
}
388+
355389
site.RenderStaticErrorPage(w, r, site.ErrorPageData{
356-
Status: http.StatusBadGateway,
357-
Title: "Bad Gateway",
358-
Description: "Failed to proxy request to application: " + err.Error(),
359-
RetryEnabled: true,
360-
DashboardURL: dashboardURL.String(),
390+
Status: http.StatusBadGateway,
391+
Title: "Bad Gateway",
392+
Description: desc,
393+
RetryEnabled: true,
394+
DashboardURL: dashboardURL.String(),
395+
AdditionalInfo: additionalInfo,
396+
AdditionalButtonLink: additionalButtonLink,
397+
AdditionalButtonText: additionalButtonText,
361398
})
362399
}
363400
proxy.Director = s.director(agentID, proxy.Director)

coderd/tailnet_test.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/coder/coder/v2/agent/agenttest"
2727
"github.com/coder/coder/v2/agent/proto"
2828
"github.com/coder/coder/v2/coderd"
29+
"github.com/coder/coder/v2/coderd/workspaceapps/appurl"
2930
"github.com/coder/coder/v2/codersdk/agentsdk"
3031
"github.com/coder/coder/v2/codersdk/workspacesdk"
3132
"github.com/coder/coder/v2/tailnet"
@@ -81,7 +82,7 @@ func TestServerTailnet_ReverseProxy_ProxyEnv(t *testing.T) {
8182
u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", workspacesdk.AgentHTTPAPIServerPort))
8283
require.NoError(t, err)
8384

84-
rp := serverTailnet.ReverseProxy(u, u, a.id)
85+
rp := serverTailnet.ReverseProxy(u, u, a.id, appurl.ApplicationURL{}, "")
8586

8687
rw := httptest.NewRecorder()
8788
req := httptest.NewRequest(
@@ -112,7 +113,7 @@ func TestServerTailnet_ReverseProxy(t *testing.T) {
112113
u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", workspacesdk.AgentHTTPAPIServerPort))
113114
require.NoError(t, err)
114115

115-
rp := serverTailnet.ReverseProxy(u, u, a.id)
116+
rp := serverTailnet.ReverseProxy(u, u, a.id, appurl.ApplicationURL{}, "")
116117

117118
rw := httptest.NewRecorder()
118119
req := httptest.NewRequest(
@@ -143,7 +144,7 @@ func TestServerTailnet_ReverseProxy(t *testing.T) {
143144
u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", workspacesdk.AgentHTTPAPIServerPort))
144145
require.NoError(t, err)
145146

146-
rp := serverTailnet.ReverseProxy(u, u, a.id)
147+
rp := serverTailnet.ReverseProxy(u, u, a.id, appurl.ApplicationURL{}, "")
147148

148149
rw := httptest.NewRecorder()
149150
req := httptest.NewRequest(
@@ -177,7 +178,7 @@ func TestServerTailnet_ReverseProxy(t *testing.T) {
177178
u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", workspacesdk.AgentHTTPAPIServerPort))
178179
require.NoError(t, err)
179180

180-
rp := serverTailnet.ReverseProxy(u, u, a.id)
181+
rp := serverTailnet.ReverseProxy(u, u, a.id, appurl.ApplicationURL{}, "")
181182

182183
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
183184
require.NoError(t, err)
@@ -222,7 +223,7 @@ func TestServerTailnet_ReverseProxy(t *testing.T) {
222223
u, err := url.Parse("http://127.0.0.1" + port)
223224
require.NoError(t, err)
224225

225-
rp := serverTailnet.ReverseProxy(u, u, a.id)
226+
rp := serverTailnet.ReverseProxy(u, u, a.id, appurl.ApplicationURL{}, "")
226227

227228
for i := 0; i < 5; i++ {
228229
rw := httptest.NewRecorder()
@@ -279,7 +280,7 @@ func TestServerTailnet_ReverseProxy(t *testing.T) {
279280
require.NoError(t, err)
280281

281282
for i, ag := range agents {
282-
rp := serverTailnet.ReverseProxy(u, u, ag.id)
283+
rp := serverTailnet.ReverseProxy(u, u, ag.id, appurl.ApplicationURL{}, "")
283284

284285
rw := httptest.NewRecorder()
285286
req := httptest.NewRequest(
@@ -317,7 +318,7 @@ func TestServerTailnet_ReverseProxy(t *testing.T) {
317318
uri, err := url.Parse(s.URL)
318319
require.NoError(t, err)
319320

320-
rp := serverTailnet.ReverseProxy(uri, uri, a.id)
321+
rp := serverTailnet.ReverseProxy(uri, uri, a.id, appurl.ApplicationURL{}, "")
321322

322323
rw := httptest.NewRecorder()
323324
req := httptest.NewRequest(
@@ -347,7 +348,7 @@ func TestServerTailnet_ReverseProxy(t *testing.T) {
347348
u, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", workspacesdk.AgentHTTPAPIServerPort))
348349
require.NoError(t, err)
349350

350-
rp := serverTailnet.ReverseProxy(u, u, a.id)
351+
rp := serverTailnet.ReverseProxy(u, u, a.id, appurl.ApplicationURL{}, "")
351352

352353
rw := httptest.NewRecorder()
353354
req := httptest.NewRequest(

coderd/userauth.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,7 @@ func (api *API) userAuthMethods(rw http.ResponseWriter, r *http.Request) {
472472
}
473473

474474
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.AuthMethods{
475+
TermsOfServiceURL: api.DeploymentValues.TermsOfServiceURL.Value(),
475476
Password: codersdk.AuthMethod{
476477
Enabled: !api.DeploymentValues.DisablePasswordAuth.Value(),
477478
},

coderd/workspaceapps/appurl/appurl.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"net"
66
"net/url"
77
"regexp"
8+
"strconv"
89
"strings"
910

1011
"golang.org/x/xerrors"
@@ -83,6 +84,55 @@ func (a ApplicationURL) Path() string {
8384
return fmt.Sprintf("/@%s/%s.%s/apps/%s", a.Username, a.WorkspaceName, a.AgentName, a.AppSlugOrPort)
8485
}
8586

87+
// PortInfo returns the port, protocol, and whether the AppSlugOrPort is a port or not.
88+
func (a ApplicationURL) PortInfo() (uint, string, bool) {
89+
var (
90+
port uint64
91+
protocol string
92+
isPort bool
93+
err error
94+
)
95+
96+
if strings.HasSuffix(a.AppSlugOrPort, "s") {
97+
trimmed := strings.TrimSuffix(a.AppSlugOrPort, "s")
98+
port, err = strconv.ParseUint(trimmed, 10, 16)
99+
if err == nil {
100+
protocol = "https"
101+
isPort = true
102+
}
103+
} else {
104+
port, err = strconv.ParseUint(a.AppSlugOrPort, 10, 16)
105+
if err == nil {
106+
protocol = "http"
107+
isPort = true
108+
}
109+
}
110+
111+
return uint(port), protocol, isPort
112+
}
113+
114+
func (a *ApplicationURL) ChangePortProtocol(target string) ApplicationURL {
115+
newAppURL := *a
116+
port, protocol, isPort := a.PortInfo()
117+
if !isPort {
118+
return newAppURL
119+
}
120+
121+
if target == protocol {
122+
return newAppURL
123+
}
124+
125+
if target == "https" {
126+
newAppURL.AppSlugOrPort = fmt.Sprintf("%ds", port)
127+
}
128+
129+
if target == "http" {
130+
newAppURL.AppSlugOrPort = fmt.Sprintf("%d", port)
131+
}
132+
133+
return newAppURL
134+
}
135+
86136
// ParseSubdomainAppURL parses an ApplicationURL from the given subdomain. If
87137
// the subdomain is not a valid application URL hostname, returns a non-nil
88138
// error. If the hostname is not a subdomain of the given base hostname, returns

0 commit comments

Comments
 (0)