Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d6a1217

Browse files
committed
Add option validation
1 parent c5225ae commit d6a1217

File tree

4 files changed

+213
-51
lines changed

4 files changed

+213
-51
lines changed

codersdk/workspaceproxy.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,9 @@ import (
1212
)
1313

1414
type WorkspaceProxy struct {
15-
ID uuid.UUID `db:"id" json:"id" format:"uuid"`
16-
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id" format:"uuid"`
17-
Name string `db:"name" json:"name"`
18-
Icon string `db:"icon" json:"icon"`
15+
ID uuid.UUID `db:"id" json:"id" format:"uuid"`
16+
Name string `db:"name" json:"name"`
17+
Icon string `db:"icon" json:"icon"`
1918
// Full url including scheme of the proxy api url: https://us.example.com
2019
URL string `db:"url" json:"url"`
2120
// WildcardHostname with the wildcard for subdomain based app hosting: *.us.example.com
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package coderdenttest
2+
3+
import (
4+
"context"
5+
"crypto/tls"
6+
"fmt"
7+
"net"
8+
"net/http"
9+
"net/http/httptest"
10+
"net/url"
11+
"regexp"
12+
"sync"
13+
"testing"
14+
15+
"github.com/coder/coder/codersdk"
16+
"github.com/moby/moby/pkg/namesgenerator"
17+
18+
"github.com/coder/coder/enterprise/coderd"
19+
20+
"github.com/coder/coder/enterprise/wsproxy"
21+
22+
"github.com/coder/coder/coderd/httpapi"
23+
"github.com/stretchr/testify/require"
24+
25+
"github.com/coder/coder/coderd/coderdtest"
26+
27+
"github.com/coder/coder/coderd/rbac"
28+
"github.com/prometheus/client_golang/prometheus"
29+
30+
"cdr.dev/slog"
31+
"cdr.dev/slog/sloggers/slogtest"
32+
"github.com/coder/coder/coderd/database"
33+
"github.com/coder/coder/coderd/database/dbauthz"
34+
"github.com/coder/coder/coderd/database/dbtestutil"
35+
)
36+
37+
type ProxyOptions struct {
38+
Name string
39+
40+
Database database.Store
41+
Pubsub database.Pubsub
42+
Authorizer rbac.Authorizer
43+
TLSCertificates []tls.Certificate
44+
ProxyURL *url.URL
45+
AppHostname string
46+
}
47+
48+
// NewWorkspaceProxy will configure a wsproxy.Server with the given options.
49+
// The new wsproxy will register itself with the given coderd.API instance.
50+
// The first user owner client is required to create the wsproxy on the coderd
51+
// api server.
52+
func NewWorkspaceProxy(t *testing.T, coderd *coderd.API, owner *codersdk.Client, options *ProxyOptions) *wsproxy.Server {
53+
ctx, cancelFunc := context.WithCancel(context.Background())
54+
t.Cleanup(cancelFunc)
55+
56+
if options == nil {
57+
options = &ProxyOptions{}
58+
}
59+
60+
if options.Authorizer == nil {
61+
options.Authorizer = &coderdtest.RecordingAuthorizer{
62+
Wrapped: rbac.NewCachingAuthorizer(prometheus.NewRegistry()),
63+
}
64+
}
65+
66+
if options.Database == nil {
67+
options.Database, options.Pubsub = dbtestutil.NewDB(t)
68+
options.Database = dbauthz.New(options.Database, options.Authorizer, slogtest.Make(t, nil).Leveled(slog.LevelDebug))
69+
}
70+
71+
// HTTP Server
72+
var mutex sync.RWMutex
73+
var handler http.Handler
74+
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
75+
mutex.RLock()
76+
defer mutex.RUnlock()
77+
if handler != nil {
78+
handler.ServeHTTP(w, r)
79+
}
80+
}))
81+
srv.Config.BaseContext = func(_ net.Listener) context.Context {
82+
return ctx
83+
}
84+
if options.TLSCertificates != nil {
85+
srv.TLS = &tls.Config{
86+
Certificates: options.TLSCertificates,
87+
MinVersion: tls.VersionTLS12,
88+
}
89+
srv.StartTLS()
90+
} else {
91+
srv.Start()
92+
}
93+
t.Cleanup(srv.Close)
94+
95+
tcpAddr, ok := srv.Listener.Addr().(*net.TCPAddr)
96+
require.True(t, ok)
97+
98+
serverURL, err := url.Parse(srv.URL)
99+
require.NoError(t, err)
100+
101+
serverURL.Host = fmt.Sprintf("localhost:%d", tcpAddr.Port)
102+
103+
accessURL := options.ProxyURL
104+
if accessURL == nil {
105+
accessURL = serverURL
106+
}
107+
108+
// TODO: Stun and derp stuff
109+
//derpPort, err := strconv.Atoi(serverURL.Port())
110+
//require.NoError(t, err)
111+
//
112+
//stunAddr, stunCleanup := stuntest.ServeWithPacketListener(t, nettype.Std{})
113+
//t.Cleanup(stunCleanup)
114+
//
115+
//derpServer := derp.NewServer(key.NewNode(), tailnet.Logger(slogtest.Make(t, nil).Named("derp").Leveled(slog.LevelDebug)))
116+
//derpServer.SetMeshKey("test-key")
117+
118+
var appHostnameRegex *regexp.Regexp
119+
if options.AppHostname != "" {
120+
var err error
121+
appHostnameRegex, err = httpapi.CompileHostnamePattern(options.AppHostname)
122+
require.NoError(t, err)
123+
}
124+
125+
if options.Name == "" {
126+
options.Name = namesgenerator.GetRandomName(1)
127+
}
128+
129+
proxyRes, err := owner.CreateWorkspaceProxy(ctx, codersdk.CreateWorkspaceProxyRequest{
130+
Name: options.Name,
131+
Icon: "/emojis/flag.png",
132+
URL: accessURL.String(),
133+
WildcardHostname: options.AppHostname,
134+
})
135+
136+
wssrv, err := wsproxy.New(&wsproxy.Options{
137+
Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
138+
PrimaryAccessURL: coderd.AccessURL,
139+
AccessURL: options.ProxyURL,
140+
AppHostname: options.AppHostname,
141+
AppHostnameRegex: appHostnameRegex,
142+
RealIPConfig: coderd.RealIPConfig,
143+
AppSecurityKey: coderd.AppSecurityKey,
144+
Tracing: coderd.TracerProvider,
145+
APIRateLimit: coderd.APIRateLimit,
146+
SecureAuthCookie: coderd.SecureAuthCookie,
147+
ProxySessionToken: proxyRes.ProxyToken,
148+
// We need a new registry to not conflict with the coderd internal
149+
// proxy metrics.
150+
PrometheusRegistry: prometheus.NewRegistry(),
151+
})
152+
require.NoError(t, err)
153+
return wssrv
154+
}

enterprise/wsproxy/proxy.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import (
44
"context"
55
"net/http"
66
"net/url"
7+
"reflect"
78
"regexp"
9+
"strings"
810
"time"
911

1012
"github.com/go-chi/chi/v5"
@@ -59,6 +61,24 @@ type Options struct {
5961
ProxySessionToken string
6062
}
6163

64+
func (o *Options) Validate() error {
65+
var errs optErrors
66+
67+
errs.Required("Logger", o.Logger)
68+
errs.Required("PrimaryAccessURL", o.PrimaryAccessURL)
69+
errs.Required("AccessURL", o.AccessURL)
70+
errs.Required("RealIPConfig", o.RealIPConfig)
71+
errs.Required("Tracing", o.Tracing)
72+
errs.Required("PrometheusRegistry", o.PrometheusRegistry)
73+
errs.NotEmpty("ProxySessionToken", o.ProxySessionToken)
74+
errs.NotEmpty("AppSecurityKey", o.AppSecurityKey)
75+
76+
if len(errs) > 0 {
77+
return errs
78+
}
79+
return nil
80+
}
81+
6282
// Server is an external workspace proxy server. This server can communicate
6383
// directly with a workspace. It requires a primary coderd to establish a said
6484
// connection.
@@ -92,6 +112,10 @@ func New(opts *Options) (*Server, error) {
92112
opts.PrometheusRegistry = prometheus.NewRegistry()
93113
}
94114

115+
if err := opts.Validate(); err != nil {
116+
return nil, err
117+
}
118+
95119
client := wsproxysdk.New(opts.PrimaryAccessURL)
96120
// TODO: @emyrk we need to implement some form of authentication for the
97121
// external proxy to the the primary. This allows us to make workspace
@@ -196,3 +220,25 @@ func (s *Server) Close() error {
196220
func (s *Server) DialWorkspaceAgent(id uuid.UUID) (*codersdk.WorkspaceAgentConn, error) {
197221
return s.SDKClient.DialWorkspaceAgent(s.ctx, id, nil)
198222
}
223+
224+
type optErrors []error
225+
226+
func (e optErrors) Error() string {
227+
var b strings.Builder
228+
for _, err := range e {
229+
b.WriteString(err.Error())
230+
b.WriteString("\n")
231+
}
232+
return b.String()
233+
}
234+
235+
func (e *optErrors) Required(name string, v any) {
236+
if v == nil {
237+
*e = append(*e, xerrors.Errorf("%s is required, got <nil>", name))
238+
}
239+
}
240+
func (e *optErrors) NotEmpty(name string, v any) {
241+
if reflect.ValueOf(v).IsZero() {
242+
*e = append(*e, xerrors.Errorf("%s is required, got the zero value", name))
243+
}
244+
}

enterprise/wsproxy/proxy_test.go

Lines changed: 10 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,9 @@
11
package wsproxy_test
22

33
import (
4-
"context"
54
"net"
65
"testing"
76

8-
"github.com/coder/coder/coderd/httpapi"
9-
"github.com/coder/coder/enterprise/wsproxy"
10-
11-
"github.com/moby/moby/pkg/namesgenerator"
12-
"github.com/stretchr/testify/require"
13-
147
"github.com/coder/coder/enterprise/coderd/license"
158

169
"github.com/coder/coder/codersdk"
@@ -39,8 +32,9 @@ func TestExternalProxyWorkspaceApps(t *testing.T) {
3932
client, _, api := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
4033
Options: &coderdtest.Options{
4134
DeploymentValues: deploymentValues,
42-
// TODO: @emyrk Should we give a hostname here too?
43-
AppHostname: "",
35+
// TODO: @emyrk This hostname should be for the external
36+
// proxy, not the internal one.
37+
AppHostname: opts.AppHost,
4438
IncludeProvisionerDaemon: true,
4539
RealIPConfig: &httpmw.RealIPConfig{
4640
TrustedOrigins: []*net.IPNet{{
@@ -62,46 +56,15 @@ func TestExternalProxyWorkspaceApps(t *testing.T) {
6256
})
6357

6458
// Create the external proxy
65-
// TODO: @emyrk this code will probably change as we create a better
66-
// method of creating external proxies.
67-
ctx := context.Background()
68-
proxyRes, err := client.CreateWorkspaceProxy(ctx, codersdk.CreateWorkspaceProxyRequest{
69-
Name: namesgenerator.GetRandomName(1),
70-
Icon: "/emojis/flag.png",
71-
URL: "https://" + namesgenerator.GetRandomName(1) + ".com",
72-
WildcardHostname: opts.AppHost,
73-
})
74-
require.NoError(t, err)
75-
76-
appHostRegex, err := httpapi.CompileHostnamePattern(opts.AppHost)
77-
require.NoError(t, err, "app host regex should compile")
78-
79-
// Make the external proxy service
80-
proxy, err := wsproxy.New(&wsproxy.Options{
81-
Logger: api.Logger,
82-
PrimaryAccessURL: api.AccessURL,
83-
// TODO: @emyrk give this an access url
84-
AccessURL: nil,
85-
AppHostname: opts.AppHost,
86-
AppHostnameRegex: appHostRegex,
87-
RealIPConfig: api.RealIPConfig,
88-
AppSecurityKey: api.AppSecurityKey,
89-
Tracing: api.TracerProvider,
90-
PrometheusRegistry: api.PrometheusRegistry,
91-
APIRateLimit: api.APIRateLimit,
92-
SecureAuthCookie: api.SecureAuthCookie,
93-
ProxySessionToken: proxyRes.ProxyToken,
94-
})
95-
require.NoError(t, err, "wsproxy should be created")
96-
97-
// TODO: Run the wsproxy, http.Serve
98-
_ = proxy
59+
proxyAPI := coderdenttest.NewWorkspaceProxy(t, api, client, &coderdenttest.ProxyOptions{})
60+
var _ = proxyAPI
9961

10062
return &apptest.Deployment{
101-
Options: opts,
102-
Client: client,
103-
FirstUser: user,
104-
PathAppBaseURL: client.URL,
63+
Options: opts,
64+
Client: client,
65+
FirstUser: user,
66+
//PathAppBaseURL: api.AccessURL,
67+
PathAppBaseURL: proxyAPI.AppServer.AccessURL,
10568
}
10669
})
10770
}

0 commit comments

Comments
 (0)