mirror of https://github.com/coder/coder.git
feat: add regions endpoint for proxies feature (#7277)
* feat: add regions endpoint for proxies feature
This commit is contained in:
parent
6e8ff2d95c
commit
a98341612c
|
@ -1727,6 +1727,31 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/regions": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"WorkspaceProxies"
|
||||
],
|
||||
"summary": "Get site-wide regions for workspace connections",
|
||||
"operationId": "get-site-wide-regions-for-workspace-connections",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.RegionsResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/replicas": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -8316,6 +8341,46 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"codersdk.Region": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"healthy": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"icon_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path_app_url": {
|
||||
"description": "PathAppURL is the URL to the base path for path apps. Optional\nunless wildcard_hostname is set.\nE.g. https://us.example.com",
|
||||
"type": "string"
|
||||
},
|
||||
"wildcard_hostname": {
|
||||
"description": "WildcardHostname is the wildcard hostname for subdomain apps.\nE.g. *.us.example.com\nE.g. *--suffix.au.example.com\nOptional. Does not need to be on the same domain as PathAppURL.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.RegionsResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"regions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Region"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.Replica": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -1501,6 +1501,27 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/regions": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["WorkspaceProxies"],
|
||||
"summary": "Get site-wide regions for workspace connections",
|
||||
"operationId": "get-site-wide-regions-for-workspace-connections",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.RegionsResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/replicas": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -7455,6 +7476,46 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"codersdk.Region": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"healthy": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"icon_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path_app_url": {
|
||||
"description": "PathAppURL is the URL to the base path for path apps. Optional\nunless wildcard_hostname is set.\nE.g. https://us.example.com",
|
||||
"type": "string"
|
||||
},
|
||||
"wildcard_hostname": {
|
||||
"description": "WildcardHostname is the wildcard hostname for subdomain apps.\nE.g. *.us.example.com\nE.g. *--suffix.au.example.com\nOptional. Does not need to be on the same domain as PathAppURL.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.RegionsResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"regions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.Region"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.Replica": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -461,6 +461,11 @@ func New(options *Options) *API {
|
|||
r.Post("/csp/reports", api.logReportCSPViolations)
|
||||
|
||||
r.Get("/buildinfo", buildInfo(api.AccessURL))
|
||||
// /regions is overridden in the enterprise version
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(apiKeyMiddleware)
|
||||
r.Get("/regions", api.regions)
|
||||
})
|
||||
r.Route("/deployment", func(r chi.Router) {
|
||||
r.Use(apiKeyMiddleware)
|
||||
r.Get("/config", api.deploymentValues)
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
package coderd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/codersdk"
|
||||
)
|
||||
|
||||
func (api *API) PrimaryRegion(ctx context.Context) (codersdk.Region, error) {
|
||||
deploymentIDStr, err := api.Database.GetDeploymentID(ctx)
|
||||
if xerrors.Is(err, sql.ErrNoRows) {
|
||||
// This shouldn't happen but it's pretty easy to avoid this causing
|
||||
// issues by falling back to a nil UUID.
|
||||
deploymentIDStr = uuid.Nil.String()
|
||||
} else if err != nil {
|
||||
return codersdk.Region{}, xerrors.Errorf("get deployment ID: %w", err)
|
||||
}
|
||||
deploymentID, err := uuid.Parse(deploymentIDStr)
|
||||
if err != nil {
|
||||
// This also shouldn't happen but we fallback to nil UUID.
|
||||
deploymentID = uuid.Nil
|
||||
}
|
||||
|
||||
return codersdk.Region{
|
||||
ID: deploymentID,
|
||||
// TODO: provide some way to customize these fields for the primary
|
||||
// region
|
||||
Name: "primary",
|
||||
DisplayName: "Default",
|
||||
IconURL: "/emojis/1f60e.png", // face with sunglasses
|
||||
Healthy: true,
|
||||
PathAppURL: api.AccessURL.String(),
|
||||
WildcardHostname: api.AppHostname,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// @Summary Get site-wide regions for workspace connections
|
||||
// @ID get-site-wide-regions-for-workspace-connections
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags WorkspaceProxies
|
||||
// @Success 200 {object} codersdk.RegionsResponse
|
||||
// @Router /regions [get]
|
||||
func (api *API) regions(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
//nolint:gocritic // this route intentionally requests resources that users
|
||||
// cannot usually access in order to give them a full list of available
|
||||
// regions.
|
||||
ctx = dbauthz.AsSystemRestricted(ctx)
|
||||
|
||||
region, err := api.PrimaryRegion(ctx)
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.RegionsResponse{
|
||||
Regions: []codersdk.Region{region},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package coderd_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/coderd/database/dbtestutil"
|
||||
"github.com/coder/coder/codersdk"
|
||||
"github.com/coder/coder/testutil"
|
||||
)
|
||||
|
||||
func TestRegions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
const appHostname = "*.apps.coder.test"
|
||||
|
||||
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 := coderdtest.New(t, &coderdtest.Options{
|
||||
AppHostname: appHostname,
|
||||
Database: db,
|
||||
Pubsub: pubsub,
|
||||
})
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
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, "primary", regions[0].Name)
|
||||
require.Equal(t, "Default", regions[0].DisplayName)
|
||||
require.NotEmpty(t, regions[0].IconURL)
|
||||
require.True(t, regions[0].Healthy)
|
||||
require.Equal(t, client.URL.String(), regions[0].PathAppURL)
|
||||
require.Equal(t, appHostname, regions[0].WildcardHostname)
|
||||
|
||||
// Ensure the primary region ID is constant.
|
||||
regions2, err := client.Regions(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, regions[0].ID, regions2[0].ID)
|
||||
})
|
||||
|
||||
t.Run("RequireAuth", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
unauthedClient := codersdk.New(client.URL)
|
||||
regions, err := unauthedClient.Regions(ctx)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, regions)
|
||||
})
|
||||
}
|
|
@ -130,3 +130,44 @@ func (c *Client) DeleteWorkspaceProxyByName(ctx context.Context, name string) er
|
|||
func (c *Client) DeleteWorkspaceProxyByID(ctx context.Context, id uuid.UUID) error {
|
||||
return c.DeleteWorkspaceProxyByName(ctx, id.String())
|
||||
}
|
||||
|
||||
type RegionsResponse struct {
|
||||
Regions []Region `json:"regions"`
|
||||
}
|
||||
|
||||
type Region struct {
|
||||
ID uuid.UUID `json:"id" format:"uuid"`
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
IconURL string `json:"icon_url"`
|
||||
Healthy bool `json:"healthy"`
|
||||
|
||||
// PathAppURL is the URL to the base path for path apps. Optional
|
||||
// unless wildcard_hostname is set.
|
||||
// E.g. https://us.example.com
|
||||
PathAppURL string `json:"path_app_url"`
|
||||
|
||||
// WildcardHostname is the wildcard hostname for subdomain apps.
|
||||
// E.g. *.us.example.com
|
||||
// E.g. *--suffix.au.example.com
|
||||
// Optional. Does not need to be on the same domain as PathAppURL.
|
||||
WildcardHostname string `json:"wildcard_hostname"`
|
||||
}
|
||||
|
||||
func (c *Client) Regions(ctx context.Context) ([]Region, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet,
|
||||
"/api/v2/regions",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("make request: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, ReadBodyAsError(res)
|
||||
}
|
||||
|
||||
var regions RegionsResponse
|
||||
return regions.Regions, json.NewDecoder(res.Body).Decode(®ions)
|
||||
}
|
||||
|
|
|
@ -3480,6 +3480,56 @@ Parameter represents a set value for the scope.
|
|||
| `api` | integer | false | | |
|
||||
| `disable_all` | boolean | false | | |
|
||||
|
||||
## codersdk.Region
|
||||
|
||||
```json
|
||||
{
|
||||
"display_name": "string",
|
||||
"healthy": true,
|
||||
"icon_url": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"name": "string",
|
||||
"path_app_url": "string",
|
||||
"wildcard_hostname": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ------------------- | ------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `display_name` | string | false | | |
|
||||
| `healthy` | boolean | false | | |
|
||||
| `icon_url` | string | false | | |
|
||||
| `id` | string | false | | |
|
||||
| `name` | string | false | | |
|
||||
| `path_app_url` | string | false | | Path app URL is the URL to the base path for path apps. Optional unless wildcard_hostname is set. E.g. https://us.example.com |
|
||||
| `wildcard_hostname` | string | false | | Wildcard hostname is the wildcard hostname for subdomain apps. E.g. _.us.example.com E.g. _--suffix.au.example.com Optional. Does not need to be on the same domain as PathAppURL. |
|
||||
|
||||
## codersdk.RegionsResponse
|
||||
|
||||
```json
|
||||
{
|
||||
"regions": [
|
||||
{
|
||||
"display_name": "string",
|
||||
"healthy": true,
|
||||
"icon_url": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"name": "string",
|
||||
"path_app_url": "string",
|
||||
"wildcard_hostname": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| --------- | ------------------------------------------- | -------- | ------------ | ----------- |
|
||||
| `regions` | array of [codersdk.Region](#codersdkregion) | false | | |
|
||||
|
||||
## codersdk.Replica
|
||||
|
||||
```json
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
# WorkspaceProxies
|
||||
|
||||
## Get site-wide regions for workspace connections
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X GET http://coder-server:8080/api/v2/regions \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`GET /regions`
|
||||
|
||||
### Example responses
|
||||
|
||||
> 200 Response
|
||||
|
||||
```json
|
||||
{
|
||||
"regions": [
|
||||
{
|
||||
"display_name": "string",
|
||||
"healthy": true,
|
||||
"icon_url": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"name": "string",
|
||||
"path_app_url": "string",
|
||||
"wildcard_hostname": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------------------- |
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.RegionsResponse](schemas.md#codersdkregionsresponse) |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
|
@ -466,6 +466,10 @@
|
|||
"title": "Users",
|
||||
"path": "./api/users.md"
|
||||
},
|
||||
{
|
||||
"title": "WorkspaceProxies",
|
||||
"path": "./api/workspaceproxies.md"
|
||||
},
|
||||
{
|
||||
"title": "Workspaces",
|
||||
"path": "./api/workspaces.md"
|
||||
|
|
|
@ -74,6 +74,11 @@ func New(ctx context.Context, options *Options) (*API, error) {
|
|||
|
||||
api.AGPL.APIHandler.Group(func(r chi.Router) {
|
||||
r.Get("/entitlements", api.serveEntitlements)
|
||||
// /regions overrides the AGPL /regions endpoint
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(apiKeyMiddleware)
|
||||
r.Get("/regions", api.regions)
|
||||
})
|
||||
r.Route("/replicas", func(r chi.Router) {
|
||||
r.Use(apiKeyMiddleware)
|
||||
r.Get("/", api.replicas)
|
||||
|
@ -231,7 +236,7 @@ func New(ctx context.Context, options *Options) (*API, error) {
|
|||
|
||||
if api.AGPL.Experiments.Enabled(codersdk.ExperimentMoons) {
|
||||
// Proxy health is a moon feature.
|
||||
api.proxyHealth, err = proxyhealth.New(&proxyhealth.Options{
|
||||
api.ProxyHealth, err = proxyhealth.New(&proxyhealth.Options{
|
||||
Interval: time.Second * 5,
|
||||
DB: api.Database,
|
||||
Logger: options.Logger.Named("proxyhealth"),
|
||||
|
@ -241,7 +246,7 @@ func New(ctx context.Context, options *Options) (*API, error) {
|
|||
if err != nil {
|
||||
return nil, xerrors.Errorf("initialize proxy health: %w", err)
|
||||
}
|
||||
go api.proxyHealth.Run(ctx)
|
||||
go api.ProxyHealth.Run(ctx)
|
||||
// Force the initial loading of the cache. Do this in a go routine in case
|
||||
// the calls to the workspace proxies hang and this takes some time.
|
||||
go api.forceWorkspaceProxyHealthUpdate(ctx)
|
||||
|
@ -287,8 +292,8 @@ type API struct {
|
|||
replicaManager *replicasync.Manager
|
||||
// Meshes DERP connections from multiple replicas.
|
||||
derpMesh *derpmesh.Mesh
|
||||
// proxyHealth checks the reachability of all workspace proxies.
|
||||
proxyHealth *proxyhealth.ProxyHealth
|
||||
// ProxyHealth checks the reachability of all workspace proxies.
|
||||
ProxyHealth *proxyhealth.ProxyHealth
|
||||
|
||||
entitlementsMu sync.RWMutex
|
||||
entitlements codersdk.Entitlements
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
agpl "github.com/coder/coder/coderd"
|
||||
"github.com/coder/coder/coderd/audit"
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
|
@ -29,11 +30,60 @@ import (
|
|||
// forceWorkspaceProxyHealthUpdate forces an update of the proxy health.
|
||||
// This is useful when a proxy is created or deleted. Errors will be logged.
|
||||
func (api *API) forceWorkspaceProxyHealthUpdate(ctx context.Context) {
|
||||
if err := api.proxyHealth.ForceUpdate(ctx); err != nil {
|
||||
if err := api.ProxyHealth.ForceUpdate(ctx); err != nil {
|
||||
api.Logger.Error(ctx, "force proxy health update", slog.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: this doesn't need a swagger definition since AGPL already has one, and
|
||||
// this route overrides the AGPL one.
|
||||
func (api *API) regions(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
//nolint:gocritic // this route intentionally requests resources that users
|
||||
// cannot usually access in order to give them a full list of available
|
||||
// regions.
|
||||
ctx = dbauthz.AsSystemRestricted(ctx)
|
||||
|
||||
primaryRegion, err := api.AGPL.PrimaryRegion(ctx)
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
regions := []codersdk.Region{primaryRegion}
|
||||
|
||||
proxies, err := api.Database.GetWorkspaceProxies(ctx)
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
proxyHealth := api.ProxyHealth.HealthStatus()
|
||||
for _, proxy := range proxies {
|
||||
if proxy.Deleted {
|
||||
continue
|
||||
}
|
||||
|
||||
health, ok := proxyHealth[proxy.ID]
|
||||
if !ok {
|
||||
health.Status = proxyhealth.Unknown
|
||||
}
|
||||
|
||||
regions = append(regions, codersdk.Region{
|
||||
ID: proxy.ID,
|
||||
Name: proxy.Name,
|
||||
DisplayName: proxy.DisplayName,
|
||||
IconURL: proxy.Icon,
|
||||
Healthy: health.Status == proxyhealth.Healthy,
|
||||
PathAppURL: proxy.Url,
|
||||
WildcardHostname: proxy.WildcardHostname,
|
||||
})
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.RegionsResponse{
|
||||
Regions: regions,
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Delete workspace proxy
|
||||
// @ID delete-workspace-proxy
|
||||
// @Security CoderSessionToken
|
||||
|
@ -180,7 +230,7 @@ func (api *API) workspaceProxies(rw http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
statues := api.proxyHealth.HealthStatus()
|
||||
statues := api.ProxyHealth.HealthStatus()
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertProxies(proxies, statues))
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,152 @@ import (
|
|||
"github.com/coder/coder/testutil"
|
||||
)
|
||||
|
||||
func TestRegions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const appHostname = "*.apps.coder.test"
|
||||
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dv := coderdtest.DeploymentValues(t)
|
||||
dv.Experiments = []string{
|
||||
string(codersdk.ExperimentMoons),
|
||||
"*",
|
||||
}
|
||||
|
||||
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{
|
||||
AppHostname: appHostname,
|
||||
Database: db,
|
||||
Pubsub: pubsub,
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
})
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
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, "primary", regions[0].Name)
|
||||
require.Equal(t, "Default", regions[0].DisplayName)
|
||||
require.NotEmpty(t, regions[0].IconURL)
|
||||
require.True(t, regions[0].Healthy)
|
||||
require.Equal(t, client.URL.String(), regions[0].PathAppURL)
|
||||
require.Equal(t, appHostname, regions[0].WildcardHostname)
|
||||
|
||||
// Ensure the primary region ID is constant.
|
||||
regions2, err := client.Regions(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, regions[0].ID, regions2[0].ID)
|
||||
})
|
||||
|
||||
t.Run("WithProxies", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dv := coderdtest.DeploymentValues(t)
|
||||
dv.Experiments = []string{
|
||||
string(codersdk.ExperimentMoons),
|
||||
"*",
|
||||
}
|
||||
|
||||
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{
|
||||
AppHostname: appHostname,
|
||||
Database: db,
|
||||
Pubsub: pubsub,
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
_ = closer.Close()
|
||||
})
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
_ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureWorkspaceProxy: 1,
|
||||
},
|
||||
})
|
||||
|
||||
const proxyName = "hello"
|
||||
_ = coderdenttest.NewWorkspaceProxy(t, api, client, &coderdenttest.ProxyOptions{
|
||||
Name: proxyName,
|
||||
AppHostname: appHostname + ".proxy",
|
||||
})
|
||||
proxy, err := db.GetWorkspaceProxyByName(ctx, proxyName)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Refresh proxy health.
|
||||
err = api.ProxyHealth.ForceUpdate(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
regions, err := client.Regions(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, regions, 2)
|
||||
|
||||
// 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, "primary", regions[0].Name)
|
||||
require.Equal(t, "Default", regions[0].DisplayName)
|
||||
require.NotEmpty(t, regions[0].IconURL)
|
||||
require.True(t, regions[0].Healthy)
|
||||
require.Equal(t, client.URL.String(), regions[0].PathAppURL)
|
||||
require.Equal(t, appHostname, regions[0].WildcardHostname)
|
||||
|
||||
// Region 1 is the proxy.
|
||||
require.NotEqual(t, uuid.Nil, regions[1].ID)
|
||||
require.Equal(t, proxy.ID, regions[1].ID)
|
||||
require.Equal(t, proxy.Name, regions[1].Name)
|
||||
require.Equal(t, proxy.DisplayName, regions[1].DisplayName)
|
||||
require.Equal(t, proxy.Icon, regions[1].IconURL)
|
||||
require.True(t, regions[1].Healthy)
|
||||
require.Equal(t, proxy.Url, regions[1].PathAppURL)
|
||||
require.Equal(t, proxy.WildcardHostname, regions[1].WildcardHostname)
|
||||
})
|
||||
|
||||
t.Run("RequireAuth", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dv := coderdtest.DeploymentValues(t)
|
||||
dv.Experiments = []string{
|
||||
string(codersdk.ExperimentMoons),
|
||||
"*",
|
||||
}
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
client := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
AppHostname: appHostname,
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
})
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
unauthedClient := codersdk.New(client.URL)
|
||||
regions, err := unauthedClient.Regions(ctx)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, regions)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWorkspaceProxyCRUD(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
|
|
@ -707,6 +707,22 @@ export interface RateLimitConfig {
|
|||
readonly api: number
|
||||
}
|
||||
|
||||
// From codersdk/workspaceproxy.go
|
||||
export interface Region {
|
||||
readonly id: string
|
||||
readonly name: string
|
||||
readonly display_name: string
|
||||
readonly icon_url: string
|
||||
readonly healthy: boolean
|
||||
readonly path_app_url: string
|
||||
readonly wildcard_hostname: string
|
||||
}
|
||||
|
||||
// From codersdk/workspaceproxy.go
|
||||
export interface RegionsResponse {
|
||||
readonly regions: Region[]
|
||||
}
|
||||
|
||||
// From codersdk/replicas.go
|
||||
export interface Replica {
|
||||
readonly id: string
|
||||
|
|
Loading…
Reference in New Issue