chore: make default workspace proxy editable (#7903)

* chore: add editing the default workspace proxy
This commit is contained in:
Steven Masley
2023-06-08 10:30:15 -05:00
committed by GitHub
parent 5e647ba07a
commit fa8153a0fd
20 changed files with 453 additions and 52 deletions
+11
View File
@@ -10,6 +10,7 @@ import (
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"database/sql"
"encoding/base64"
"encoding/json"
"encoding/pem"
@@ -206,6 +207,16 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
options.Database = dbauthz.New(options.Database, options.Authorizer, slogtest.Make(t, nil).Leveled(slog.LevelDebug))
}
// Some routes expect a deployment ID, so just make sure one exists.
// Check first incase the caller already set up this database.
// nolint:gocritic // Setting up unit test data inside test helper
depID, err := options.Database.GetDeploymentID(dbauthz.AsSystemRestricted(context.Background()))
if xerrors.Is(err, sql.ErrNoRows) || depID == "" {
// nolint:gocritic // Setting up unit test data inside test helper
err := options.Database.InsertDeploymentID(dbauthz.AsSystemRestricted(context.Background()), uuid.NewString())
require.NoError(t, err, "insert a deployment id")
}
if options.DeploymentValues == nil {
options.DeploymentValues = DeploymentValues(t)
}
+5
View File
@@ -369,6 +369,11 @@ func (q *querier) DeleteLicense(ctx context.Context, id int32) (int32, error) {
return id, nil
}
func (q *querier) GetDefaultProxyConfig(ctx context.Context) (database.GetDefaultProxyConfigRow, error) {
// No authz checks
return q.db.GetDefaultProxyConfig(ctx)
}
func (q *querier) GetDeploymentID(ctx context.Context) (string, error) {
// No authz checks
return q.db.GetDeploymentID(ctx)
+6
View File
@@ -335,6 +335,12 @@ func (s *MethodTestSuite) TestLicense() {
s.Run("GetDeploymentID", s.Subtest(func(db database.Store, check *expects) {
check.Args().Asserts().Returns("")
}))
s.Run("GetDefaultProxyConfig", s.Subtest(func(db database.Store, check *expects) {
check.Args().Asserts().Returns(database.GetDefaultProxyConfigRow{
DisplayName: "Default",
IconUrl: "/emojis/1f3e1.png",
})
}))
s.Run("GetLogoURL", s.Subtest(func(db database.Store, check *expects) {
err := db.UpsertLogoURL(context.Background(), "value")
require.NoError(s.T(), err)
+7
View File
@@ -431,3 +431,10 @@ func (q *querier) GetWorkspaceProxyByHostname(ctx context.Context, params databa
}
return q.db.GetWorkspaceProxyByHostname(ctx, params)
}
func (q *querier) UpsertDefaultProxy(ctx context.Context, arg database.UpsertDefaultProxyParams) error {
if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceSystem); err != nil {
return err
}
return q.db.UpsertDefaultProxy(ctx, arg)
}
+3
View File
@@ -25,6 +25,9 @@ func (s *MethodTestSuite) TestSystemFunctions() {
LoginType: database.LoginTypeGithub,
}).Asserts(rbac.ResourceSystem, rbac.ActionUpdate).Returns(l)
}))
s.Run("UpsertDefaultProxy", s.Subtest(func(db database.Store, check *expects) {
check.Args(database.UpsertDefaultProxyParams{}).Asserts(rbac.ResourceSystem, rbac.ActionUpdate).Returns()
}))
s.Run("GetUserLinkByLinkedID", s.Subtest(func(db database.Store, check *expects) {
l := dbgen.UserLink(s.T(), db, database.UserLink{})
check.Args(l.LinkedID).Asserts(rbac.ResourceSystem, rbac.ActionRead).Returns(l)
+27 -9
View File
@@ -41,7 +41,7 @@ var errDuplicateKey = &pq.Error{
// New returns an in-memory fake of the database.
func New() database.Store {
return &fakeQuerier{
q := &fakeQuerier{
mutex: &sync.RWMutex{},
data: &data{
apiKeys: make([]database.APIKey, 0),
@@ -73,6 +73,9 @@ func New() database.Store {
locks: map[int64]struct{}{},
},
}
q.defaultProxyDisplayName = "Default"
q.defaultProxyIconURL = "/emojis/1f3e1.png"
return q
}
type rwMutex interface {
@@ -144,14 +147,16 @@ type data struct {
// Locks is a map of lock names. Any keys within the map are currently
// locked.
locks map[int64]struct{}
deploymentID string
derpMeshKey string
lastUpdateCheck []byte
serviceBanner []byte
logoURL string
appSecurityKey string
lastLicenseID int32
locks map[int64]struct{}
deploymentID string
derpMeshKey string
lastUpdateCheck []byte
serviceBanner []byte
logoURL string
appSecurityKey string
lastLicenseID int32
defaultProxyDisplayName string
defaultProxyIconURL string
}
func validateDatabaseTypeWithValid(v reflect.Value) (handled bool, err error) {
@@ -5171,3 +5176,16 @@ func isNull(v interface{}) bool {
func isNotNull(v interface{}) bool {
return reflect.ValueOf(v).FieldByName("Valid").Bool()
}
func (q *fakeQuerier) GetDefaultProxyConfig(_ context.Context) (database.GetDefaultProxyConfigRow, error) {
return database.GetDefaultProxyConfigRow{
DisplayName: q.defaultProxyDisplayName,
IconUrl: q.defaultProxyIconURL,
}, nil
}
func (q *fakeQuerier) UpsertDefaultProxy(_ context.Context, arg database.UpsertDefaultProxyParams) error {
q.defaultProxyDisplayName = arg.DisplayName
q.defaultProxyIconURL = arg.IconUrl
return nil
}
+14
View File
@@ -1518,3 +1518,17 @@ func (m metricsStore) GetAuthorizedUserCount(ctx context.Context, arg database.G
m.queryLatencies.WithLabelValues("GetAuthorizedUserCount").Observe(time.Since(start).Seconds())
return count, err
}
func (m metricsStore) UpsertDefaultProxy(ctx context.Context, arg database.UpsertDefaultProxyParams) error {
start := time.Now()
err := m.s.UpsertDefaultProxy(ctx, arg)
m.queryLatencies.WithLabelValues("UpsertDefaultProxy").Observe(time.Since(start).Seconds())
return err
}
func (m metricsStore) GetDefaultProxyConfig(ctx context.Context) (database.GetDefaultProxyConfigRow, error) {
start := time.Now()
resp, err := m.s.GetDefaultProxyConfig(ctx)
m.queryLatencies.WithLabelValues("GetDefaultProxyConfig").Observe(time.Since(start).Seconds())
return resp, err
}
+29
View File
@@ -418,6 +418,21 @@ func (mr *MockStoreMockRecorder) GetDERPMeshKey(arg0 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDERPMeshKey", reflect.TypeOf((*MockStore)(nil).GetDERPMeshKey), arg0)
}
// GetDefaultProxyConfig mocks base method.
func (m *MockStore) GetDefaultProxyConfig(arg0 context.Context) (database.GetDefaultProxyConfigRow, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDefaultProxyConfig", arg0)
ret0, _ := ret[0].(database.GetDefaultProxyConfigRow)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetDefaultProxyConfig indicates an expected call of GetDefaultProxyConfig.
func (mr *MockStoreMockRecorder) GetDefaultProxyConfig(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDefaultProxyConfig", reflect.TypeOf((*MockStore)(nil).GetDefaultProxyConfig), arg0)
}
// GetDeploymentDAUs mocks base method.
func (m *MockStore) GetDeploymentDAUs(arg0 context.Context, arg1 int32) ([]database.GetDeploymentDAUsRow, error) {
m.ctrl.T.Helper()
@@ -3088,6 +3103,20 @@ func (mr *MockStoreMockRecorder) UpsertAppSecurityKey(arg0, arg1 interface{}) *g
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertAppSecurityKey", reflect.TypeOf((*MockStore)(nil).UpsertAppSecurityKey), arg0, arg1)
}
// UpsertDefaultProxy mocks base method.
func (m *MockStore) UpsertDefaultProxy(arg0 context.Context, arg1 database.UpsertDefaultProxyParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpsertDefaultProxy", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// UpsertDefaultProxy indicates an expected call of UpsertDefaultProxy.
func (mr *MockStoreMockRecorder) UpsertDefaultProxy(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertDefaultProxy", reflect.TypeOf((*MockStore)(nil).UpsertDefaultProxy), arg0, arg1)
}
// UpsertLastUpdateCheck mocks base method.
func (m *MockStore) UpsertLastUpdateCheck(arg0 context.Context, arg1 string) error {
m.ctrl.T.Helper()
+4
View File
@@ -186,6 +186,10 @@ func (w WorkspaceProxy) RBACObject() rbac.Object {
WithID(w.ID)
}
func (w WorkspaceProxy) IsPrimary() bool {
return w.Name == "primary"
}
func (f File) RBACObject() rbac.Object {
return rbac.ResourceFile.
WithID(f.ID).
+5
View File
@@ -54,6 +54,7 @@ type sqlcQuerier interface {
// are included.
GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUID) (GetAuthorizationUserRolesRow, error)
GetDERPMeshKey(ctx context.Context) (string, error)
GetDefaultProxyConfig(ctx context.Context) (GetDefaultProxyConfigRow, error)
GetDeploymentDAUs(ctx context.Context, tzOffset int32) ([]GetDeploymentDAUsRow, error)
GetDeploymentID(ctx context.Context) (string, error)
GetDeploymentWorkspaceAgentStats(ctx context.Context, createdAt time.Time) (GetDeploymentWorkspaceAgentStatsRow, error)
@@ -254,6 +255,10 @@ type sqlcQuerier interface {
UpdateWorkspaceTTL(ctx context.Context, arg UpdateWorkspaceTTLParams) error
UpdateWorkspaceTTLToBeWithinTemplateMax(ctx context.Context, arg UpdateWorkspaceTTLToBeWithinTemplateMaxParams) error
UpsertAppSecurityKey(ctx context.Context, value string) error
// The default proxy is implied and not actually stored in the database.
// So we need to store it's configuration here for display purposes.
// The functional values are immutable and controlled implicitly.
UpsertDefaultProxy(ctx context.Context, arg UpsertDefaultProxyParams) error
UpsertLastUpdateCheck(ctx context.Context, value string) error
UpsertLogoURL(ctx context.Context, value string) error
UpsertServiceBanner(ctx context.Context, value string) error
+55
View File
@@ -14,6 +14,7 @@ import (
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/dbgen"
"github.com/coder/coder/coderd/database/migrations"
"github.com/coder/coder/testutil"
)
func TestGetDeploymentWorkspaceAgentStats(t *testing.T) {
@@ -257,3 +258,57 @@ func TestProxyByHostname(t *testing.T) {
})
}
}
func TestDefaultProxy(t *testing.T) {
t.Parallel()
if testing.Short() {
t.SkipNow()
}
sqlDB := testSQLDB(t)
err := migrations.Up(sqlDB)
require.NoError(t, err)
db := database.New(sqlDB)
ctx := testutil.Context(t, testutil.WaitLong)
depID := uuid.NewString()
err = db.InsertDeploymentID(ctx, depID)
require.NoError(t, err, "insert deployment id")
// Fetch empty proxy values
defProxy, err := db.GetDefaultProxyConfig(ctx)
require.NoError(t, err, "get def proxy")
require.Equal(t, defProxy.DisplayName, "Default")
require.Equal(t, defProxy.IconUrl, "/emojis/1f3e1.png")
// Set the proxy values
args := database.UpsertDefaultProxyParams{
DisplayName: "displayname",
IconUrl: "/icon.png",
}
err = db.UpsertDefaultProxy(ctx, args)
require.NoError(t, err, "insert def proxy")
defProxy, err = db.GetDefaultProxyConfig(ctx)
require.NoError(t, err, "get def proxy")
require.Equal(t, defProxy.DisplayName, args.DisplayName)
require.Equal(t, defProxy.IconUrl, args.IconUrl)
// Upsert values
args = database.UpsertDefaultProxyParams{
DisplayName: "newdisplayname",
IconUrl: "/newicon.png",
}
err = db.UpsertDefaultProxy(ctx, args)
require.NoError(t, err, "upsert def proxy")
defProxy, err = db.GetDefaultProxyConfig(ctx)
require.NoError(t, err, "get def proxy")
require.Equal(t, defProxy.DisplayName, args.DisplayName)
require.Equal(t, defProxy.IconUrl, args.IconUrl)
// Ensure other site configs are the same
found, err := db.GetDeploymentID(ctx)
require.NoError(t, err, "get deployment id")
require.Equal(t, depID, found)
}
+41
View File
@@ -3036,6 +3036,24 @@ func (q *sqlQuerier) GetDERPMeshKey(ctx context.Context) (string, error) {
return value, err
}
const getDefaultProxyConfig = `-- name: GetDefaultProxyConfig :one
SELECT
COALESCE((SELECT value FROM site_configs WHERE key = 'default_proxy_display_name'), 'Default') :: text AS display_name,
COALESCE((SELECT value FROM site_configs WHERE key = 'default_proxy_icon_url'), '/emojis/1f3e1.png') :: text AS icon_url
`
type GetDefaultProxyConfigRow struct {
DisplayName string `db:"display_name" json:"display_name"`
IconUrl string `db:"icon_url" json:"icon_url"`
}
func (q *sqlQuerier) GetDefaultProxyConfig(ctx context.Context) (GetDefaultProxyConfigRow, error) {
row := q.db.QueryRowContext(ctx, getDefaultProxyConfig)
var i GetDefaultProxyConfigRow
err := row.Scan(&i.DisplayName, &i.IconUrl)
return i, err
}
const getDeploymentID = `-- name: GetDeploymentID :one
SELECT value FROM site_configs WHERE key = 'deployment_id'
`
@@ -3108,6 +3126,29 @@ func (q *sqlQuerier) UpsertAppSecurityKey(ctx context.Context, value string) err
return err
}
const upsertDefaultProxy = `-- name: UpsertDefaultProxy :exec
INSERT INTO site_configs (key, value)
VALUES
('default_proxy_display_name', $1 :: text),
('default_proxy_icon_url', $2 :: text)
ON CONFLICT
(key)
DO UPDATE SET value = EXCLUDED.value WHERE site_configs.key = EXCLUDED.key
`
type UpsertDefaultProxyParams struct {
DisplayName string `db:"display_name" json:"display_name"`
IconUrl string `db:"icon_url" json:"icon_url"`
}
// The default proxy is implied and not actually stored in the database.
// So we need to store it's configuration here for display purposes.
// The functional values are immutable and controlled implicitly.
func (q *sqlQuerier) UpsertDefaultProxy(ctx context.Context, arg UpsertDefaultProxyParams) error {
_, err := q.db.ExecContext(ctx, upsertDefaultProxy, arg.DisplayName, arg.IconUrl)
return err
}
const upsertLastUpdateCheck = `-- name: UpsertLastUpdateCheck :exec
INSERT INTO site_configs (key, value) VALUES ('last_update_check', $1)
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'last_update_check'
+20
View File
@@ -1,3 +1,23 @@
-- name: UpsertDefaultProxy :exec
-- The default proxy is implied and not actually stored in the database.
-- So we need to store it's configuration here for display purposes.
-- The functional values are immutable and controlled implicitly.
INSERT INTO site_configs (key, value)
VALUES
('default_proxy_display_name', @display_name :: text),
('default_proxy_icon_url', @icon_url :: text)
ON CONFLICT
(key)
DO UPDATE SET value = EXCLUDED.value WHERE site_configs.key = EXCLUDED.key
;
-- name: GetDefaultProxyConfig :one
SELECT
COALESCE((SELECT value FROM site_configs WHERE key = 'default_proxy_display_name'), 'Default') :: text AS display_name,
COALESCE((SELECT value FROM site_configs WHERE key = 'default_proxy_icon_url'), '/emojis/1f3e1.png') :: text AS icon_url
;
-- name: InsertDeploymentID :exec
INSERT INTO site_configs (key, value) VALUES ('deployment_id', $1);
+7 -2
View File
@@ -173,7 +173,7 @@ func WorkspaceProxyParam(r *http.Request) database.WorkspaceProxy {
// parameter.
//
//nolint:revive
func ExtractWorkspaceProxyParam(db database.Store) func(http.Handler) http.Handler {
func ExtractWorkspaceProxyParam(db database.Store, deploymentID string, fetchPrimaryProxy func(ctx context.Context) (database.WorkspaceProxy, error)) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -188,9 +188,14 @@ func ExtractWorkspaceProxyParam(db database.Store) func(http.Handler) http.Handl
var proxy database.WorkspaceProxy
var dbErr error
if proxyID, err := uuid.Parse(proxyQuery); err == nil {
if proxyQuery == "primary" || proxyQuery == deploymentID {
// Requesting primary proxy
proxy, dbErr = fetchPrimaryProxy(ctx)
} else if proxyID, err := uuid.Parse(proxyQuery); err == nil {
// Request proxy by id
proxy, dbErr = db.GetWorkspaceProxyByID(ctx, proxyID)
} else {
// Request proxy by name
proxy, dbErr = db.GetWorkspaceProxyByName(ctx, proxyQuery)
}
if httpapi.Is404Error(dbErr) {
+38 -3
View File
@@ -212,7 +212,7 @@ func TestExtractWorkspaceProxyParam(t *testing.T) {
routeContext.URLParams.Add("workspaceproxy", proxy.Name)
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, routeContext))
httpmw.ExtractWorkspaceProxyParam(db)(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
httpmw.ExtractWorkspaceProxyParam(db, uuid.NewString(), nil)(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
// Checks that it exists on the context!
_ = httpmw.WorkspaceProxyParam(request)
successHandler.ServeHTTP(writer, request)
@@ -236,7 +236,7 @@ func TestExtractWorkspaceProxyParam(t *testing.T) {
routeContext.URLParams.Add("workspaceproxy", proxy.ID.String())
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, routeContext))
httpmw.ExtractWorkspaceProxyParam(db)(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
httpmw.ExtractWorkspaceProxyParam(db, uuid.NewString(), nil)(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
// Checks that it exists on the context!
_ = httpmw.WorkspaceProxyParam(request)
successHandler.ServeHTTP(writer, request)
@@ -258,9 +258,44 @@ func TestExtractWorkspaceProxyParam(t *testing.T) {
routeContext.URLParams.Add("workspaceproxy", uuid.NewString())
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, routeContext))
httpmw.ExtractWorkspaceProxyParam(db)(successHandler).ServeHTTP(rw, r)
httpmw.ExtractWorkspaceProxyParam(db, uuid.NewString(), nil)(successHandler).ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusNotFound, res.StatusCode)
})
t.Run("FetchPrimary", func(t *testing.T) {
t.Parallel()
var (
db = dbfake.New()
r = httptest.NewRequest("GET", "/", nil)
rw = httptest.NewRecorder()
deploymentID = uuid.New()
primaryProxy = database.WorkspaceProxy{
ID: deploymentID,
Name: "primary",
DisplayName: "Default",
Icon: "Icon",
Url: "Url",
WildcardHostname: "Wildcard",
}
fetchPrimary = func(ctx context.Context) (database.WorkspaceProxy, error) {
return primaryProxy, nil
}
)
routeContext := chi.NewRouteContext()
routeContext.URLParams.Add("workspaceproxy", deploymentID.String())
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, routeContext))
httpmw.ExtractWorkspaceProxyParam(db, deploymentID.String(), fetchPrimary)(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
// Checks that it exists on the context!
found := httpmw.WorkspaceProxyParam(request)
require.Equal(t, primaryProxy, found)
successHandler.ServeHTTP(writer, request)
})).ServeHTTP(rw, r)
res := rw.Result()
defer res.Body.Close()
require.Equal(t, http.StatusOK, res.StatusCode)
})
}
+31 -5
View File
@@ -8,11 +8,14 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/dbauthz"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/codersdk"
)
// PrimaryRegion exposes the user facing values of a workspace proxy to
// be used by a user.
func (api *API) PrimaryRegion(ctx context.Context) (codersdk.Region, error) {
deploymentIDStr, err := api.Database.GetDeploymentID(ctx)
if xerrors.Is(err, sql.ErrNoRows) {
@@ -28,19 +31,42 @@ func (api *API) PrimaryRegion(ctx context.Context) (codersdk.Region, error) {
deploymentID = uuid.Nil
}
proxy, err := api.Database.GetDefaultProxyConfig(ctx)
if err != nil {
return codersdk.Region{}, xerrors.Errorf("get default proxy config: %w", err)
}
return codersdk.Region{
ID: deploymentID,
// TODO: provide some way to customize these fields for the primary
// region
ID: deploymentID,
Name: "primary",
DisplayName: "Default",
IconURL: "/emojis/1f60e.png", // face with sunglasses
DisplayName: proxy.DisplayName,
IconURL: proxy.IconUrl,
Healthy: true,
PathAppURL: api.AccessURL.String(),
WildcardHostname: api.AppHostname,
}, nil
}
// PrimaryWorkspaceProxy returns the primary workspace proxy for the site.
func (api *API) PrimaryWorkspaceProxy(ctx context.Context) (database.WorkspaceProxy, error) {
region, err := api.PrimaryRegion(ctx)
if err != nil {
return database.WorkspaceProxy{}, err
}
// The default proxy is an edge case because these values are computed
// rather then being stored in the database.
return database.WorkspaceProxy{
ID: region.ID,
Name: region.Name,
DisplayName: region.DisplayName,
Icon: region.IconURL,
Url: region.PathAppURL,
WildcardHostname: region.WildcardHostname,
Deleted: false,
}, nil
}
// @Summary Get site-wide regions for workspace connections
// @ID get-site-wide-regions-for-workspace-connections
// @Security CoderSessionToken
+10 -3
View File
@@ -82,8 +82,15 @@ func Test_ProxyCRUD(t *testing.T) {
// Also check via the api
proxies, err := client.WorkspaceProxies(ctx)
require.NoError(t, err, "failed to get workspace proxies")
require.Len(t, proxies, 1, "expected 1 proxy")
require.Equal(t, expectedName, proxies[0].Name, "expected proxy name to match")
// Include primary
require.Len(t, proxies, 2, "expected 1 proxy")
found := false
for _, proxy := range proxies {
if proxy.Name == expectedName {
found = true
}
}
require.True(t, found, "expected proxy to be found")
})
t.Run("Delete", func(t *testing.T) {
@@ -130,6 +137,6 @@ func Test_ProxyCRUD(t *testing.T) {
proxies, err := client.WorkspaceProxies(ctx)
require.NoError(t, err, "failed to get workspace proxies")
require.Len(t, proxies, 0, "expected no proxies")
require.Len(t, proxies, 1, "expected only primary proxy")
})
}
+6 -2
View File
@@ -72,6 +72,11 @@ func New(ctx context.Context, options *Options) (*API, error) {
RedirectToLogin: false,
})
deploymentID, err := options.Database.GetDeploymentID(ctx)
if err != nil {
return nil, xerrors.Errorf("failed to get deployment ID: %w", err)
}
api.AGPL.APIHandler.Group(func(r chi.Router) {
r.Get("/entitlements", api.serveEntitlements)
// /regions overrides the AGPL /regions endpoint
@@ -118,7 +123,7 @@ func New(ctx context.Context, options *Options) (*API, error) {
r.Route("/{workspaceproxy}", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
httpmw.ExtractWorkspaceProxyParam(api.Database),
httpmw.ExtractWorkspaceProxyParam(api.Database, deploymentID, api.AGPL.PrimaryWorkspaceProxy),
)
r.Get("/", api.workspaceProxy)
@@ -225,7 +230,6 @@ func New(ctx context.Context, options *Options) (*API, error) {
RootCAs: meshRootCA,
ServerName: options.AccessURL.Hostname(),
}
var err error
api.replicaManager, err = replicasync.New(ctx, options.Logger, options.Database, options.Pubsub, &replicasync.Options{
ID: api.AGPL.ID,
RelayAddress: options.DERPServerRelayAddress,
+117 -12
View File
@@ -127,23 +127,39 @@ func (api *API) patchWorkspaceProxy(rw http.ResponseWriter, r *http.Request) {
}
}
updatedProxy, err := api.Database.UpdateWorkspaceProxy(ctx, database.UpdateWorkspaceProxyParams{
Name: req.Name,
DisplayName: req.DisplayName,
Icon: req.Icon,
ID: proxy.ID,
// If hashedSecret is nil or empty, this will not update the secret.
TokenHashedSecret: hashedSecret,
})
if httpapi.Is404Error(err) {
httpapi.ResourceNotFound(rw)
return
}
deploymentIDStr, err := api.Database.GetDeploymentID(ctx)
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
var updatedProxy database.WorkspaceProxy
if proxy.ID.String() == deploymentIDStr {
// User is editing the default primary proxy.
var ok bool
updatedProxy, ok = api.patchPrimaryWorkspaceProxy(req, rw, r)
if !ok {
return
}
} else {
updatedProxy, err = api.Database.UpdateWorkspaceProxy(ctx, database.UpdateWorkspaceProxyParams{
Name: req.Name,
DisplayName: req.DisplayName,
Icon: req.Icon,
ID: proxy.ID,
// If hashedSecret is nil or empty, this will not update the secret.
TokenHashedSecret: hashedSecret,
})
if httpapi.Is404Error(err) {
httpapi.ResourceNotFound(rw)
return
}
if err != nil {
httpapi.InternalServerError(rw, err)
return
}
}
aReq.New = updatedProxy
status, ok := api.ProxyHealth.HealthStatus()[updatedProxy.ID]
if !ok {
@@ -159,6 +175,68 @@ func (api *API) patchWorkspaceProxy(rw http.ResponseWriter, r *http.Request) {
go api.forceWorkspaceProxyHealthUpdate(api.ctx)
}
// patchPrimaryWorkspaceProxy handles the special case of updating the default
func (api *API) patchPrimaryWorkspaceProxy(req codersdk.PatchWorkspaceProxy, rw http.ResponseWriter, r *http.Request) (database.WorkspaceProxy, bool) {
var (
ctx = r.Context()
proxy = httpmw.WorkspaceProxyParam(r)
)
// User is editing the default primary proxy.
if req.Name != "" && req.Name != proxy.Name {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Cannot update name of default primary proxy, did you mean to update the 'display name'?",
Validations: []codersdk.ValidationError{
{Field: "name", Detail: "Cannot update name of default primary proxy"},
},
})
return database.WorkspaceProxy{}, false
}
if req.DisplayName == "" && req.Icon == "" {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "No update arguments provided. Nothing to do.",
Validations: []codersdk.ValidationError{
{Field: "display_name", Detail: "No value provided."},
{Field: "icon", Detail: "No value provided."},
},
})
return database.WorkspaceProxy{}, false
}
args := database.UpsertDefaultProxyParams{
DisplayName: req.DisplayName,
IconUrl: req.Icon,
}
if req.DisplayName == "" || req.Icon == "" {
// If the user has not specified an update value, use the existing value.
existing, err := api.Database.GetDefaultProxyConfig(ctx)
if err != nil {
httpapi.InternalServerError(rw, err)
return database.WorkspaceProxy{}, false
}
if req.DisplayName == "" {
args.DisplayName = existing.DisplayName
}
if req.Icon == "" {
args.IconUrl = existing.IconUrl
}
}
err := api.Database.UpsertDefaultProxy(ctx, args)
if err != nil {
httpapi.InternalServerError(rw, err)
return database.WorkspaceProxy{}, false
}
// Use the primary region to fetch the default proxy values.
updatedProxy, err := api.AGPL.PrimaryWorkspaceProxy(ctx)
if err != nil {
httpapi.InternalServerError(rw, err)
return database.WorkspaceProxy{}, false
}
return updatedProxy, true
}
// @Summary Delete workspace proxy
// @ID delete-workspace-proxy
// @Security CoderSessionToken
@@ -182,6 +260,13 @@ func (api *API) deleteWorkspaceProxy(rw http.ResponseWriter, r *http.Request) {
aReq.Old = proxy
defer commitAudit()
if proxy.IsPrimary() {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Cannot delete primary proxy",
})
return
}
err := api.Database.UpdateWorkspaceProxyDeleted(ctx, database.UpdateWorkspaceProxyDeletedParams{
ID: proxy.ID,
Deleted: true,
@@ -334,6 +419,14 @@ func (api *API) workspaceProxies(rw http.ResponseWriter, r *http.Request) {
return
}
// Add the primary as well
primaryProxy, err := api.AGPL.PrimaryWorkspaceProxy(ctx)
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
httpapi.InternalServerError(rw, err)
return
}
proxies = append([]database.WorkspaceProxy{primaryProxy}, proxies...)
statues := api.ProxyHealth.HealthStatus()
httpapi.Write(ctx, rw, http.StatusOK, convertProxies(proxies, statues))
}
@@ -611,6 +704,18 @@ func convertProxies(p []database.WorkspaceProxy, statuses map[uuid.UUID]proxyhea
}
func convertProxy(p database.WorkspaceProxy, status proxyhealth.ProxyStatus) codersdk.WorkspaceProxy {
if p.IsPrimary() {
// Primary is always healthy since the primary serves the api that this
// is returned from.
u, _ := url.Parse(p.Url)
status = proxyhealth.ProxyStatus{
Proxy: p,
ProxyHost: u.Host,
Status: proxyhealth.Healthy,
Report: codersdk.ProxyHealthReport{},
CheckedAt: time.Now(),
}
}
if status.Status == "" {
status.Status = proxyhealth.Unknown
}
+17 -16
View File
@@ -44,11 +44,6 @@ func TestRegions(t *testing.T) {
}
db, pubsub := dbtestutil.NewDB(t)
deploymentID := uuid.New()
ctx := testutil.Context(t, testutil.WaitLong)
err := db.InsertDeploymentID(ctx, deploymentID.String())
require.NoError(t, err)
client := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{
@@ -58,14 +53,18 @@ func TestRegions(t *testing.T) {
DeploymentValues: dv,
},
})
_ = coderdtest.CreateFirstUser(t, client)
ctx := testutil.Context(t, testutil.WaitLong)
deploymentID, err := db.GetDeploymentID(ctx)
require.NoError(t, err, "get deployment ID")
regions, err := client.Regions(ctx)
require.NoError(t, err)
require.Len(t, regions, 1)
require.NotEqual(t, uuid.Nil, regions[0].ID)
require.Equal(t, regions[0].ID, deploymentID)
require.Equal(t, regions[0].ID.String(), deploymentID)
require.Equal(t, "primary", regions[0].Name)
require.Equal(t, "Default", regions[0].DisplayName)
require.NotEmpty(t, regions[0].IconURL)
@@ -89,11 +88,6 @@ func TestRegions(t *testing.T) {
}
db, pubsub := dbtestutil.NewDB(t)
deploymentID := uuid.New()
ctx := testutil.Context(t, testutil.WaitLong)
err := db.InsertDeploymentID(ctx, deploymentID.String())
require.NoError(t, err)
client, closer, api := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
Options: &coderdtest.Options{
@@ -106,6 +100,9 @@ func TestRegions(t *testing.T) {
t.Cleanup(func() {
_ = closer.Close()
})
ctx := testutil.Context(t, testutil.WaitLong)
deploymentID, err := db.GetDeploymentID(ctx)
require.NoError(t, err, "get deployment ID")
_ = coderdtest.CreateFirstUser(t, client)
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
Features: license.Features{
@@ -131,7 +128,7 @@ func TestRegions(t *testing.T) {
// Region 0 is the primary require.Len(t, regions, 1)
require.NotEqual(t, uuid.Nil, regions[0].ID)
require.Equal(t, regions[0].ID, deploymentID)
require.Equal(t, regions[0].ID.String(), deploymentID)
require.Equal(t, "primary", regions[0].Name)
require.Equal(t, "Default", regions[0].DisplayName)
require.NotEmpty(t, regions[0].IconURL)
@@ -325,7 +322,8 @@ func TestWorkspaceProxyCRUD(t *testing.T) {
proxies, err := client.WorkspaceProxies(ctx)
require.NoError(t, err)
require.Len(t, proxies, 0)
// Default proxy is always there
require.Len(t, proxies, 1)
})
}
@@ -387,11 +385,10 @@ func TestIssueSignedAppToken(t *testing.T) {
})
require.NoError(t, err)
proxyClient := wsproxysdk.New(client.URL)
proxyClient.SetSessionToken(proxyRes.ProxyToken)
t.Run("BadAppRequest", func(t *testing.T) {
t.Parallel()
proxyClient := wsproxysdk.New(client.URL)
proxyClient.SetSessionToken(proxyRes.ProxyToken)
ctx := testutil.Context(t, testutil.WaitLong)
_, err = proxyClient.IssueSignedAppToken(ctx, workspaceapps.IssueTokenRequest{
@@ -412,6 +409,8 @@ func TestIssueSignedAppToken(t *testing.T) {
}
t.Run("OK", func(t *testing.T) {
t.Parallel()
proxyClient := wsproxysdk.New(client.URL)
proxyClient.SetSessionToken(proxyRes.ProxyToken)
ctx := testutil.Context(t, testutil.WaitLong)
_, err = proxyClient.IssueSignedAppToken(ctx, goodRequest)
@@ -420,6 +419,8 @@ func TestIssueSignedAppToken(t *testing.T) {
t.Run("OKHTML", func(t *testing.T) {
t.Parallel()
proxyClient := wsproxysdk.New(client.URL)
proxyClient.SetSessionToken(proxyRes.ProxyToken)
rw := httptest.NewRecorder()
ctx := testutil.Context(t, testutil.WaitLong)