Skip to content

feat: add regions endpoint for proxies feature #7277

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 25, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
67 changes: 67 additions & 0 deletions coderd/workspaceproxies.go
Original file line number Diff line number Diff line change
@@ -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},
})
}
67 changes: 67 additions & 0 deletions coderd/workspaceproxies_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
41 changes: 41 additions & 0 deletions codersdk/workspaceproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(&regions)
}
13 changes: 9 additions & 4 deletions enterprise/coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"),
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
54 changes: 52 additions & 2 deletions enterprise/coderd/workspaceproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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))
}

Expand Down
Loading