feat: add regions endpoint for proxies feature (#7277)

* feat: add regions endpoint for proxies feature
This commit is contained in:
Dean Sheather
2023-04-25 07:37:52 -07:00
committed by GitHub
parent 6e8ff2d95c
commit a98341612c
13 changed files with 625 additions and 6 deletions

View File

@ -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

View File

@ -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))
}

View File

@ -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()