mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: make default workspace proxy editable (#7903)
* chore: add editing the default workspace proxy
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user