Skip to content

Commit 9db95ae

Browse files
committed
Unit test to fetch primary with httpmw
1 parent 9cb6421 commit 9db95ae

File tree

5 files changed

+163
-23
lines changed

5 files changed

+163
-23
lines changed

coderd/httpmw/workspaceproxy.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ func WorkspaceProxyParam(r *http.Request) database.WorkspaceProxy {
173173
// parameter.
174174
//
175175
//nolint:revive
176-
func ExtractWorkspaceProxyParam(db database.Store) func(http.Handler) http.Handler {
176+
func ExtractWorkspaceProxyParam(db database.Store, deploymentID string, fetchPrimaryProxy func(ctx context.Context) (database.WorkspaceProxy, error)) func(http.Handler) http.Handler {
177177
return func(next http.Handler) http.Handler {
178178
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
179179
ctx := r.Context()
@@ -188,9 +188,14 @@ func ExtractWorkspaceProxyParam(db database.Store) func(http.Handler) http.Handl
188188

189189
var proxy database.WorkspaceProxy
190190
var dbErr error
191-
if proxyID, err := uuid.Parse(proxyQuery); err == nil {
191+
if proxyQuery == "primary" || proxyQuery == deploymentID {
192+
// Requesting primary proxy
193+
proxy, dbErr = fetchPrimaryProxy(ctx)
194+
} else if proxyID, err := uuid.Parse(proxyQuery); err == nil {
195+
// Request proxy by id
192196
proxy, dbErr = db.GetWorkspaceProxyByID(ctx, proxyID)
193197
} else {
198+
// Request proxy by name
194199
proxy, dbErr = db.GetWorkspaceProxyByName(ctx, proxyQuery)
195200
}
196201
if httpapi.Is404Error(dbErr) {

coderd/httpmw/workspaceproxy_test.go

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ func TestExtractWorkspaceProxyParam(t *testing.T) {
212212
routeContext.URLParams.Add("workspaceproxy", proxy.Name)
213213
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, routeContext))
214214

215-
httpmw.ExtractWorkspaceProxyParam(db)(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
215+
httpmw.ExtractWorkspaceProxyParam(db, uuid.NewString(), nil)(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
216216
// Checks that it exists on the context!
217217
_ = httpmw.WorkspaceProxyParam(request)
218218
successHandler.ServeHTTP(writer, request)
@@ -236,7 +236,7 @@ func TestExtractWorkspaceProxyParam(t *testing.T) {
236236
routeContext.URLParams.Add("workspaceproxy", proxy.ID.String())
237237
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, routeContext))
238238

239-
httpmw.ExtractWorkspaceProxyParam(db)(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
239+
httpmw.ExtractWorkspaceProxyParam(db, uuid.NewString(), nil)(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
240240
// Checks that it exists on the context!
241241
_ = httpmw.WorkspaceProxyParam(request)
242242
successHandler.ServeHTTP(writer, request)
@@ -258,9 +258,44 @@ func TestExtractWorkspaceProxyParam(t *testing.T) {
258258
routeContext.URLParams.Add("workspaceproxy", uuid.NewString())
259259
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, routeContext))
260260

261-
httpmw.ExtractWorkspaceProxyParam(db)(successHandler).ServeHTTP(rw, r)
261+
httpmw.ExtractWorkspaceProxyParam(db, uuid.NewString(), nil)(successHandler).ServeHTTP(rw, r)
262262
res := rw.Result()
263263
defer res.Body.Close()
264264
require.Equal(t, http.StatusNotFound, res.StatusCode)
265265
})
266+
267+
t.Run("FetchPrimary", func(t *testing.T) {
268+
t.Parallel()
269+
var (
270+
db = dbfake.New()
271+
r = httptest.NewRequest("GET", "/", nil)
272+
rw = httptest.NewRecorder()
273+
deploymentID = uuid.New()
274+
primaryProxy = database.WorkspaceProxy{
275+
ID: deploymentID,
276+
Name: "primary",
277+
DisplayName: "Default",
278+
Icon: "Icon",
279+
Url: "Url",
280+
WildcardHostname: "Wildcard",
281+
}
282+
fetchPrimary = func(ctx context.Context) (database.WorkspaceProxy, error) {
283+
return primaryProxy, nil
284+
}
285+
)
286+
287+
routeContext := chi.NewRouteContext()
288+
routeContext.URLParams.Add("workspaceproxy", deploymentID.String())
289+
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, routeContext))
290+
291+
httpmw.ExtractWorkspaceProxyParam(db, deploymentID.String(), fetchPrimary)(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
292+
// Checks that it exists on the context!
293+
found := httpmw.WorkspaceProxyParam(request)
294+
require.Equal(t, primaryProxy, found)
295+
successHandler.ServeHTTP(writer, request)
296+
})).ServeHTTP(rw, r)
297+
res := rw.Result()
298+
defer res.Body.Close()
299+
require.Equal(t, http.StatusOK, res.StatusCode)
300+
})
266301
}

coderd/workspaceproxies.go

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@ package coderd
33
import (
44
"context"
55
"database/sql"
6-
"net/http"
7-
86
"github.com/google/uuid"
97
"golang.org/x/xerrors"
8+
"net/http"
109

10+
"github.com/coder/coder/coderd/database"
1111
"github.com/coder/coder/coderd/database/dbauthz"
1212
"github.com/coder/coder/coderd/httpapi"
1313
"github.com/coder/coder/codersdk"
1414
)
1515

16+
// PrimaryRegion exposes the user facing values of a workspace proxy to
17+
// be used by a user.
1618
func (api *API) PrimaryRegion(ctx context.Context) (codersdk.Region, error) {
1719
deploymentIDStr, err := api.Database.GetDeploymentID(ctx)
1820
if xerrors.Is(err, sql.ErrNoRows) {
@@ -28,17 +30,42 @@ func (api *API) PrimaryRegion(ctx context.Context) (codersdk.Region, error) {
2830
deploymentID = uuid.Nil
2931
}
3032

33+
proxy, err := api.Database.GetDefaultProxyConfig(ctx)
34+
if err != nil {
35+
return codersdk.Region{}, xerrors.Errorf("get default proxy config: %w", err)
36+
}
37+
3138
return codersdk.Region{
3239
ID: deploymentID,
3340
Name: "primary",
34-
DisplayName: "Default",
35-
IconURL: "/emojis/1f3e1.png", // House with garden
41+
DisplayName: proxy.DisplayName,
42+
IconURL: proxy.IconUrl,
3643
Healthy: true,
3744
PathAppURL: api.AccessURL.String(),
3845
WildcardHostname: api.AppHostname,
3946
}, nil
4047
}
4148

49+
// PrimaryWorkspaceProxy returns the primary workspace proxy for the site.
50+
func (api *API) PrimaryWorkspaceProxy(ctx context.Context) (database.WorkspaceProxy, error) {
51+
region, err := api.PrimaryRegion(ctx)
52+
if err != nil {
53+
return database.WorkspaceProxy{}, err
54+
}
55+
56+
// The default proxy is an edge case because these values are computed
57+
// rather then being stored in the database.
58+
return database.WorkspaceProxy{
59+
ID: region.ID,
60+
Name: region.Name,
61+
DisplayName: region.DisplayName,
62+
Icon: region.IconURL,
63+
Url: region.PathAppURL,
64+
WildcardHostname: region.WildcardHostname,
65+
Deleted: false,
66+
}, nil
67+
}
68+
4269
// @Summary Get site-wide regions for workspace connections
4370
// @ID get-site-wide-regions-for-workspace-connections
4471
// @Security CoderSessionToken

enterprise/coderd/coderd.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ func New(ctx context.Context, options *Options) (*API, error) {
7272
RedirectToLogin: false,
7373
})
7474

75+
deploymentID, err := options.Database.GetDeploymentID(ctx)
76+
if err != nil {
77+
return nil, xerrors.Errorf("failed to get deployment ID: %w", err)
78+
}
79+
7580
api.AGPL.APIHandler.Group(func(r chi.Router) {
7681
r.Get("/entitlements", api.serveEntitlements)
7782
// /regions overrides the AGPL /regions endpoint
@@ -118,7 +123,7 @@ func New(ctx context.Context, options *Options) (*API, error) {
118123
r.Route("/{workspaceproxy}", func(r chi.Router) {
119124
r.Use(
120125
apiKeyMiddleware,
121-
httpmw.ExtractWorkspaceProxyParam(api.Database),
126+
httpmw.ExtractWorkspaceProxyParam(api.Database, deploymentID, api.AGPL.PrimaryWorkspaceProxy),
122127
)
123128

124129
r.Get("/", api.workspaceProxy)
@@ -225,7 +230,6 @@ func New(ctx context.Context, options *Options) (*API, error) {
225230
RootCAs: meshRootCA,
226231
ServerName: options.AccessURL.Hostname(),
227232
}
228-
var err error
229233
api.replicaManager, err = replicasync.New(ctx, options.Logger, options.Database, options.Pubsub, &replicasync.Options{
230234
ID: api.AGPL.ID,
231235
RelayAddress: options.DERPServerRelayAddress,

enterprise/coderd/workspaceproxy.go

Lines changed: 81 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -127,23 +127,85 @@ func (api *API) patchWorkspaceProxy(rw http.ResponseWriter, r *http.Request) {
127127
}
128128
}
129129

130-
updatedProxy, err := api.Database.UpdateWorkspaceProxy(ctx, database.UpdateWorkspaceProxyParams{
131-
Name: req.Name,
132-
DisplayName: req.DisplayName,
133-
Icon: req.Icon,
134-
ID: proxy.ID,
135-
// If hashedSecret is nil or empty, this will not update the secret.
136-
TokenHashedSecret: hashedSecret,
137-
})
138-
if httpapi.Is404Error(err) {
139-
httpapi.ResourceNotFound(rw)
140-
return
141-
}
130+
deploymentIDStr, err := api.Database.GetDeploymentID(ctx)
142131
if err != nil {
143132
httpapi.InternalServerError(rw, err)
144133
return
145134
}
146135

136+
var updatedProxy database.WorkspaceProxy
137+
if proxy.ID.String() == deploymentIDStr {
138+
// User is editing the default primary proxy.
139+
if req.Name != "" {
140+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
141+
Message: "Cannot update name of default primary proxy",
142+
Validations: []codersdk.ValidationError{
143+
{Field: "name", Detail: "Cannot update name of default primary proxy"},
144+
},
145+
})
146+
return
147+
}
148+
if req.DisplayName == "" && req.Icon == "" {
149+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
150+
Message: "No update arguments provided. Nothing to do.",
151+
Validations: []codersdk.ValidationError{
152+
{Field: "display_name", Detail: "No value provided."},
153+
{Field: "icon", Detail: "No value provided."},
154+
},
155+
})
156+
return
157+
}
158+
159+
args := database.UpsertDefaultProxyParams{
160+
DisplayName: req.DisplayName,
161+
IconUrl: req.Icon,
162+
}
163+
if req.DisplayName == "" || req.Icon == "" {
164+
// If the user has not specified an update value, use the existing value.
165+
existing, err := api.Database.GetDefaultProxyConfig(ctx)
166+
if err != nil {
167+
httpapi.InternalServerError(rw, err)
168+
return
169+
}
170+
if req.DisplayName == "" {
171+
args.DisplayName = existing.DisplayName
172+
}
173+
if req.Icon == "" {
174+
args.IconUrl = existing.IconUrl
175+
}
176+
}
177+
178+
err = api.Database.UpsertDefaultProxy(ctx, args)
179+
if err != nil {
180+
httpapi.InternalServerError(rw, err)
181+
return
182+
}
183+
184+
// Use the primary region to fetch the default proxy values.
185+
updatedProxy, err = api.AGPL.PrimaryWorkspaceProxy(ctx)
186+
if err != nil {
187+
httpapi.InternalServerError(rw, err)
188+
return
189+
}
190+
} else {
191+
updatedProxy, err = api.Database.UpdateWorkspaceProxy(ctx, database.UpdateWorkspaceProxyParams{
192+
Name: req.Name,
193+
DisplayName: req.DisplayName,
194+
Icon: req.Icon,
195+
ID: proxy.ID,
196+
// If hashedSecret is nil or empty, this will not update the secret.
197+
TokenHashedSecret: hashedSecret,
198+
})
199+
if httpapi.Is404Error(err) {
200+
httpapi.ResourceNotFound(rw)
201+
return
202+
}
203+
if err != nil {
204+
httpapi.InternalServerError(rw, err)
205+
return
206+
}
207+
}
208+
147209
aReq.New = updatedProxy
148210
status, ok := api.ProxyHealth.HealthStatus()[updatedProxy.ID]
149211
if !ok {
@@ -182,6 +244,13 @@ func (api *API) deleteWorkspaceProxy(rw http.ResponseWriter, r *http.Request) {
182244
aReq.Old = proxy
183245
defer commitAudit()
184246

247+
if proxy.Name == "primary" {
248+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
249+
Message: "Cannot delete primary proxy",
250+
})
251+
return
252+
}
253+
185254
err := api.Database.UpdateWorkspaceProxyDeleted(ctx, database.UpdateWorkspaceProxyDeletedParams{
186255
ID: proxy.ID,
187256
Deleted: true,

0 commit comments

Comments
 (0)