mirror of https://github.com/coder/coder.git
907 lines
29 KiB
Go
907 lines
29 KiB
Go
package coderd_test
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/moby/moby/pkg/namesgenerator"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/agent/agenttest"
|
|
"github.com/coder/coder/v2/buildinfo"
|
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
|
"github.com/coder/coder/v2/coderd/workspaceapps"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
|
"github.com/coder/coder/v2/enterprise/coderd/license"
|
|
"github.com/coder/coder/v2/enterprise/wsproxy/wsproxysdk"
|
|
"github.com/coder/coder/v2/provisioner/echo"
|
|
"github.com/coder/coder/v2/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)
|
|
|
|
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
|
Options: &coderdtest.Options{
|
|
AppHostname: appHostname,
|
|
Database: db,
|
|
Pubsub: pubsub,
|
|
DeploymentValues: dv,
|
|
},
|
|
})
|
|
|
|
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.String(), 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)
|
|
|
|
client, closer, api, _ := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
|
|
Options: &coderdtest.Options{
|
|
AppHostname: appHostname,
|
|
Database: db,
|
|
Pubsub: pubsub,
|
|
DeploymentValues: dv,
|
|
},
|
|
LicenseOptions: &coderdenttest.LicenseOptions{
|
|
Features: license.Features{
|
|
codersdk.FeatureWorkspaceProxy: 1,
|
|
},
|
|
},
|
|
})
|
|
t.Cleanup(func() {
|
|
_ = closer.Close()
|
|
})
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
deploymentID, err := db.GetDeploymentID(ctx)
|
|
require.NoError(t, err, "get deployment ID")
|
|
|
|
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.String(), 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,
|
|
},
|
|
})
|
|
|
|
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()
|
|
|
|
t.Run("CreateAndUpdate", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dv := coderdtest.DeploymentValues(t)
|
|
dv.Experiments = []string{
|
|
string(codersdk.ExperimentMoons),
|
|
"*",
|
|
}
|
|
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
|
Options: &coderdtest.Options{
|
|
DeploymentValues: dv,
|
|
},
|
|
LicenseOptions: &coderdenttest.LicenseOptions{
|
|
Features: license.Features{
|
|
codersdk.FeatureWorkspaceProxy: 1,
|
|
},
|
|
},
|
|
})
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
proxyRes, err := client.CreateWorkspaceProxy(ctx, codersdk.CreateWorkspaceProxyRequest{
|
|
Name: namesgenerator.GetRandomName(1),
|
|
Icon: "/emojis/flag.png",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
found, err := client.WorkspaceProxyByID(ctx, proxyRes.Proxy.ID)
|
|
require.NoError(t, err)
|
|
// This will be different, so set it to the same
|
|
found.Status = proxyRes.Proxy.Status
|
|
require.Equal(t, proxyRes.Proxy, found, "expected proxy")
|
|
require.NotEmpty(t, proxyRes.ProxyToken)
|
|
|
|
// Update the proxy
|
|
expName := namesgenerator.GetRandomName(1)
|
|
expDisplayName := namesgenerator.GetRandomName(1)
|
|
expIcon := namesgenerator.GetRandomName(1)
|
|
_, err = client.PatchWorkspaceProxy(ctx, codersdk.PatchWorkspaceProxy{
|
|
ID: proxyRes.Proxy.ID,
|
|
Name: expName,
|
|
DisplayName: expDisplayName,
|
|
Icon: expIcon,
|
|
})
|
|
require.NoError(t, err, "expected no error updating proxy")
|
|
|
|
found, err = client.WorkspaceProxyByID(ctx, proxyRes.Proxy.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, expName, found.Name, "name")
|
|
require.Equal(t, expDisplayName, found.DisplayName, "display name")
|
|
require.Equal(t, expIcon, found.IconURL, "icon")
|
|
})
|
|
|
|
t.Run("Delete", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dv := coderdtest.DeploymentValues(t)
|
|
dv.Experiments = []string{
|
|
string(codersdk.ExperimentMoons),
|
|
"*",
|
|
}
|
|
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
|
Options: &coderdtest.Options{
|
|
DeploymentValues: dv,
|
|
},
|
|
LicenseOptions: &coderdenttest.LicenseOptions{
|
|
Features: license.Features{
|
|
codersdk.FeatureWorkspaceProxy: 1,
|
|
},
|
|
},
|
|
})
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
proxyRes, err := client.CreateWorkspaceProxy(ctx, codersdk.CreateWorkspaceProxyRequest{
|
|
Name: namesgenerator.GetRandomName(1),
|
|
Icon: "/emojis/flag.png",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = client.DeleteWorkspaceProxyByID(ctx, proxyRes.Proxy.ID)
|
|
require.NoError(t, err, "failed to delete workspace proxy")
|
|
|
|
proxies, err := client.WorkspaceProxies(ctx)
|
|
require.NoError(t, err)
|
|
// Default proxy is always there
|
|
require.Len(t, proxies.Regions, 1)
|
|
})
|
|
}
|
|
|
|
func TestProxyRegisterDeregister(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
setup := func(t *testing.T) (*codersdk.Client, database.Store) {
|
|
dv := coderdtest.DeploymentValues(t)
|
|
dv.Experiments = []string{
|
|
string(codersdk.ExperimentMoons),
|
|
"*",
|
|
}
|
|
|
|
db, pubsub := dbtestutil.NewDB(t)
|
|
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
|
Options: &coderdtest.Options{
|
|
DeploymentValues: dv,
|
|
Database: db,
|
|
Pubsub: pubsub,
|
|
IncludeProvisionerDaemon: true,
|
|
},
|
|
ReplicaSyncUpdateInterval: time.Minute,
|
|
LicenseOptions: &coderdenttest.LicenseOptions{
|
|
Features: license.Features{
|
|
codersdk.FeatureWorkspaceProxy: 1,
|
|
},
|
|
},
|
|
})
|
|
|
|
return client, db
|
|
}
|
|
|
|
t.Run("OK", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, db := setup(t)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
const (
|
|
proxyName = "hello"
|
|
proxyDisplayName = "Hello World"
|
|
proxyIcon = "/emojis/flag.png"
|
|
)
|
|
createRes, err := client.CreateWorkspaceProxy(ctx, codersdk.CreateWorkspaceProxyRequest{
|
|
Name: proxyName,
|
|
DisplayName: proxyDisplayName,
|
|
Icon: proxyIcon,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
proxyClient := wsproxysdk.New(client.URL)
|
|
proxyClient.SetSessionToken(createRes.ProxyToken)
|
|
|
|
// Register
|
|
req := wsproxysdk.RegisterWorkspaceProxyRequest{
|
|
AccessURL: "https://proxy.coder.test",
|
|
WildcardHostname: "*.proxy.coder.test",
|
|
DerpEnabled: true,
|
|
ReplicaID: uuid.New(),
|
|
ReplicaHostname: "mars",
|
|
ReplicaError: "",
|
|
ReplicaRelayAddress: "http://127.0.0.1:8080",
|
|
Version: buildinfo.Version(),
|
|
}
|
|
registerRes1, err := proxyClient.RegisterWorkspaceProxy(ctx, req)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, registerRes1.AppSecurityKey)
|
|
require.NotEmpty(t, registerRes1.DERPMeshKey)
|
|
require.EqualValues(t, 10001, registerRes1.DERPRegionID)
|
|
require.Empty(t, registerRes1.SiblingReplicas)
|
|
|
|
proxy, err := client.WorkspaceProxyByID(ctx, createRes.Proxy.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, createRes.Proxy.ID, proxy.ID)
|
|
require.Equal(t, proxyName, proxy.Name)
|
|
require.Equal(t, proxyDisplayName, proxy.DisplayName)
|
|
require.Equal(t, proxyIcon, proxy.IconURL)
|
|
require.Equal(t, req.AccessURL, proxy.PathAppURL)
|
|
require.Equal(t, req.AccessURL, proxy.PathAppURL)
|
|
require.Equal(t, req.WildcardHostname, proxy.WildcardHostname)
|
|
require.Equal(t, req.DerpEnabled, proxy.DerpEnabled)
|
|
require.False(t, proxy.Deleted)
|
|
|
|
// Get the replica from the DB.
|
|
replica, err := db.GetReplicaByID(ctx, req.ReplicaID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, req.ReplicaID, replica.ID)
|
|
require.Equal(t, req.ReplicaHostname, replica.Hostname)
|
|
require.Equal(t, req.ReplicaError, replica.Error)
|
|
require.Equal(t, req.ReplicaRelayAddress, replica.RelayAddress)
|
|
require.Equal(t, req.Version, replica.Version)
|
|
require.EqualValues(t, 10001, replica.RegionID)
|
|
require.False(t, replica.StoppedAt.Valid)
|
|
require.Zero(t, replica.DatabaseLatency)
|
|
require.False(t, replica.Primary)
|
|
|
|
// Re-register with most fields changed.
|
|
req = wsproxysdk.RegisterWorkspaceProxyRequest{
|
|
AccessURL: "https://cool.proxy.coder.test",
|
|
WildcardHostname: "*.cool.proxy.coder.test",
|
|
DerpEnabled: false,
|
|
ReplicaID: req.ReplicaID,
|
|
ReplicaHostname: "venus",
|
|
ReplicaError: "error",
|
|
ReplicaRelayAddress: "http://127.0.0.1:9090",
|
|
Version: buildinfo.Version(),
|
|
}
|
|
registerRes2, err := proxyClient.RegisterWorkspaceProxy(ctx, req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, registerRes1, registerRes2)
|
|
|
|
// Get the proxy to ensure nothing has changed except updated_at.
|
|
proxyNew, err := client.WorkspaceProxyByID(ctx, createRes.Proxy.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, createRes.Proxy.ID, proxyNew.ID)
|
|
require.Equal(t, proxyName, proxyNew.Name)
|
|
require.Equal(t, proxyDisplayName, proxyNew.DisplayName)
|
|
require.Equal(t, proxyIcon, proxyNew.IconURL)
|
|
require.Equal(t, req.AccessURL, proxyNew.PathAppURL)
|
|
require.Equal(t, req.AccessURL, proxyNew.PathAppURL)
|
|
require.Equal(t, req.WildcardHostname, proxyNew.WildcardHostname)
|
|
require.Equal(t, req.DerpEnabled, proxyNew.DerpEnabled)
|
|
require.False(t, proxyNew.Deleted)
|
|
|
|
// Get the replica from the DB and ensure the fields have been updated,
|
|
// especially the updated_at.
|
|
replica, err = db.GetReplicaByID(ctx, req.ReplicaID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, req.ReplicaID, replica.ID)
|
|
require.Equal(t, req.ReplicaHostname, replica.Hostname)
|
|
require.Equal(t, req.ReplicaError, replica.Error)
|
|
require.Equal(t, req.ReplicaRelayAddress, replica.RelayAddress)
|
|
require.Equal(t, req.Version, replica.Version)
|
|
require.EqualValues(t, 10001, replica.RegionID)
|
|
require.False(t, replica.StoppedAt.Valid)
|
|
require.Zero(t, replica.DatabaseLatency)
|
|
require.False(t, replica.Primary)
|
|
|
|
// Deregister
|
|
err = proxyClient.DeregisterWorkspaceProxy(ctx, wsproxysdk.DeregisterWorkspaceProxyRequest{
|
|
ReplicaID: req.ReplicaID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Ensure the replica has been fully stopped.
|
|
replica, err = db.GetReplicaByID(ctx, req.ReplicaID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, req.ReplicaID, replica.ID)
|
|
require.True(t, replica.StoppedAt.Valid)
|
|
|
|
// Re-register should fail
|
|
_, err = proxyClient.RegisterWorkspaceProxy(ctx, wsproxysdk.RegisterWorkspaceProxyRequest{})
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("ReregisterUpdateReplica", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, db := setup(t)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
createRes, err := client.CreateWorkspaceProxy(ctx, codersdk.CreateWorkspaceProxyRequest{
|
|
Name: "hi",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
proxyClient := wsproxysdk.New(client.URL)
|
|
proxyClient.SetSessionToken(createRes.ProxyToken)
|
|
|
|
req := wsproxysdk.RegisterWorkspaceProxyRequest{
|
|
AccessURL: "https://proxy.coder.test",
|
|
WildcardHostname: "*.proxy.coder.test",
|
|
DerpEnabled: true,
|
|
ReplicaID: uuid.New(),
|
|
ReplicaHostname: "mars",
|
|
ReplicaError: "",
|
|
ReplicaRelayAddress: "http://127.0.0.1:8080",
|
|
Version: buildinfo.Version(),
|
|
}
|
|
_, err = proxyClient.RegisterWorkspaceProxy(ctx, req)
|
|
require.NoError(t, err)
|
|
|
|
// Get the replica from the DB.
|
|
replica, err := db.GetReplicaByID(ctx, req.ReplicaID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, req.ReplicaID, replica.ID)
|
|
|
|
time.Sleep(time.Millisecond)
|
|
|
|
// Re-register with no changed fields.
|
|
_, err = proxyClient.RegisterWorkspaceProxy(ctx, req)
|
|
require.NoError(t, err)
|
|
|
|
// Get the replica from the DB and make sure updated_at has changed.
|
|
replica, err = db.GetReplicaByID(ctx, req.ReplicaID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, req.ReplicaID, replica.ID)
|
|
require.Greater(t, replica.UpdatedAt.UnixNano(), replica.CreatedAt.UnixNano())
|
|
})
|
|
|
|
t.Run("DeregisterNonExistentReplica", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, _ := setup(t)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
createRes, err := client.CreateWorkspaceProxy(ctx, codersdk.CreateWorkspaceProxyRequest{
|
|
Name: "hi",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
proxyClient := wsproxysdk.New(client.URL)
|
|
proxyClient.SetSessionToken(createRes.ProxyToken)
|
|
|
|
err = proxyClient.DeregisterWorkspaceProxy(ctx, wsproxysdk.DeregisterWorkspaceProxyRequest{
|
|
ReplicaID: uuid.New(),
|
|
})
|
|
require.Error(t, err)
|
|
var sdkErr *codersdk.Error
|
|
require.ErrorAs(t, err, &sdkErr)
|
|
require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
|
|
})
|
|
|
|
t.Run("ReturnSiblings", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, _ := setup(t)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
createRes1, err := client.CreateWorkspaceProxy(ctx, codersdk.CreateWorkspaceProxyRequest{
|
|
Name: "one",
|
|
})
|
|
require.NoError(t, err)
|
|
createRes2, err := client.CreateWorkspaceProxy(ctx, codersdk.CreateWorkspaceProxyRequest{
|
|
Name: "two",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Register a replica on proxy 2. This shouldn't be returned by replicas
|
|
// for proxy 1.
|
|
proxyClient2 := wsproxysdk.New(client.URL)
|
|
proxyClient2.SetSessionToken(createRes2.ProxyToken)
|
|
_, err = proxyClient2.RegisterWorkspaceProxy(ctx, wsproxysdk.RegisterWorkspaceProxyRequest{
|
|
AccessURL: "https://other.proxy.coder.test",
|
|
WildcardHostname: "*.other.proxy.coder.test",
|
|
DerpEnabled: true,
|
|
ReplicaID: uuid.New(),
|
|
ReplicaHostname: "venus",
|
|
ReplicaError: "",
|
|
ReplicaRelayAddress: "http://127.0.0.1:9090",
|
|
Version: buildinfo.Version(),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Register replica 1.
|
|
proxyClient1 := wsproxysdk.New(client.URL)
|
|
proxyClient1.SetSessionToken(createRes1.ProxyToken)
|
|
req1 := wsproxysdk.RegisterWorkspaceProxyRequest{
|
|
AccessURL: "https://one.proxy.coder.test",
|
|
WildcardHostname: "*.one.proxy.coder.test",
|
|
DerpEnabled: true,
|
|
ReplicaID: uuid.New(),
|
|
ReplicaHostname: "mars1",
|
|
ReplicaError: "",
|
|
ReplicaRelayAddress: "http://127.0.0.1:8081",
|
|
Version: buildinfo.Version(),
|
|
}
|
|
registerRes1, err := proxyClient1.RegisterWorkspaceProxy(ctx, req1)
|
|
require.NoError(t, err)
|
|
require.Empty(t, registerRes1.SiblingReplicas)
|
|
|
|
// Register replica 2 and expect to get replica 1 as a sibling.
|
|
req2 := wsproxysdk.RegisterWorkspaceProxyRequest{
|
|
AccessURL: "https://two.proxy.coder.test",
|
|
WildcardHostname: "*.two.proxy.coder.test",
|
|
DerpEnabled: true,
|
|
ReplicaID: uuid.New(),
|
|
ReplicaHostname: "mars2",
|
|
ReplicaError: "",
|
|
ReplicaRelayAddress: "http://127.0.0.1:8082",
|
|
Version: buildinfo.Version(),
|
|
}
|
|
registerRes2, err := proxyClient1.RegisterWorkspaceProxy(ctx, req2)
|
|
require.NoError(t, err)
|
|
require.Len(t, registerRes2.SiblingReplicas, 1)
|
|
require.Equal(t, req1.ReplicaID, registerRes2.SiblingReplicas[0].ID)
|
|
require.Equal(t, req1.ReplicaHostname, registerRes2.SiblingReplicas[0].Hostname)
|
|
require.Equal(t, req1.ReplicaRelayAddress, registerRes2.SiblingReplicas[0].RelayAddress)
|
|
require.EqualValues(t, 10001, registerRes2.SiblingReplicas[0].RegionID)
|
|
|
|
// Re-register replica 1 and expect to get replica 2 as a sibling.
|
|
registerRes1, err = proxyClient1.RegisterWorkspaceProxy(ctx, req1)
|
|
require.NoError(t, err)
|
|
require.Len(t, registerRes1.SiblingReplicas, 1)
|
|
require.Equal(t, req2.ReplicaID, registerRes1.SiblingReplicas[0].ID)
|
|
require.Equal(t, req2.ReplicaHostname, registerRes1.SiblingReplicas[0].Hostname)
|
|
require.Equal(t, req2.ReplicaRelayAddress, registerRes1.SiblingReplicas[0].RelayAddress)
|
|
require.EqualValues(t, 10001, registerRes1.SiblingReplicas[0].RegionID)
|
|
})
|
|
|
|
// ReturnSiblings2 tries to create 100 proxy replicas and ensures that they
|
|
// all return the correct number of siblings.
|
|
t.Run("ReturnSiblings2", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, _ := setup(t)
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
createRes, err := client.CreateWorkspaceProxy(ctx, codersdk.CreateWorkspaceProxyRequest{
|
|
Name: "proxy",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
proxyClient := wsproxysdk.New(client.URL)
|
|
proxyClient.SetSessionToken(createRes.ProxyToken)
|
|
|
|
for i := 0; i < 100; i++ {
|
|
ok := false
|
|
for j := 0; j < 2; j++ {
|
|
registerRes, err := proxyClient.RegisterWorkspaceProxy(ctx, wsproxysdk.RegisterWorkspaceProxyRequest{
|
|
AccessURL: "https://proxy.coder.test",
|
|
WildcardHostname: "*.proxy.coder.test",
|
|
DerpEnabled: true,
|
|
ReplicaID: uuid.New(),
|
|
ReplicaHostname: "venus",
|
|
ReplicaError: "",
|
|
ReplicaRelayAddress: fmt.Sprintf("http://127.0.0.1:%d", 8080+i),
|
|
Version: buildinfo.Version(),
|
|
})
|
|
require.NoErrorf(t, err, "register proxy %d", i)
|
|
|
|
// If the sibling replica count is wrong, try again. The impact
|
|
// of this not being immediate is that proxies may not function
|
|
// as DERP relays until they register again in 30 seconds.
|
|
//
|
|
// In the real world, replicas will not be registering this
|
|
// quickly. Kubernetes rolls out gradually in practice.
|
|
if len(registerRes.SiblingReplicas) != i {
|
|
t.Logf("%d: expected %d siblings, got %d", i, i, len(registerRes.SiblingReplicas))
|
|
time.Sleep(100 * time.Millisecond)
|
|
continue
|
|
}
|
|
|
|
ok = true
|
|
break
|
|
}
|
|
|
|
require.True(t, ok, "expected to register replica %d", i)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestIssueSignedAppToken(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dv := coderdtest.DeploymentValues(t)
|
|
dv.Experiments = []string{
|
|
string(codersdk.ExperimentMoons),
|
|
"*",
|
|
}
|
|
|
|
db, pubsub := dbtestutil.NewDB(t)
|
|
client, user := coderdenttest.New(t, &coderdenttest.Options{
|
|
Options: &coderdtest.Options{
|
|
DeploymentValues: dv,
|
|
Database: db,
|
|
Pubsub: pubsub,
|
|
IncludeProvisionerDaemon: true,
|
|
},
|
|
LicenseOptions: &coderdenttest.LicenseOptions{
|
|
Features: license.Features{
|
|
codersdk.FeatureWorkspaceProxy: 1,
|
|
},
|
|
},
|
|
})
|
|
|
|
// Create a workspace + apps
|
|
authToken := uuid.NewString()
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
|
|
})
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
|
build := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
workspace.LatestBuild = build
|
|
|
|
// Connect an agent to the workspace
|
|
_ = agenttest.New(t, client.URL, authToken)
|
|
_ = coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
|
|
|
|
createProxyCtx := testutil.Context(t, testutil.WaitLong)
|
|
proxyRes, err := client.CreateWorkspaceProxy(createProxyCtx, codersdk.CreateWorkspaceProxyRequest{
|
|
Name: namesgenerator.GetRandomName(1),
|
|
Icon: "/emojis/flag.png",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
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{
|
|
// Invalid request.
|
|
AppRequest: workspaceapps.Request{},
|
|
SessionToken: client.SessionToken(),
|
|
})
|
|
require.Error(t, err)
|
|
})
|
|
|
|
goodRequest := workspaceapps.IssueTokenRequest{
|
|
AppRequest: workspaceapps.Request{
|
|
BasePath: "/app",
|
|
AccessMethod: workspaceapps.AccessMethodTerminal,
|
|
AgentNameOrID: build.Resources[0].Agents[0].ID.String(),
|
|
},
|
|
SessionToken: client.SessionToken(),
|
|
}
|
|
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)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
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)
|
|
_, ok := proxyClient.IssueSignedAppTokenHTML(ctx, rw, goodRequest)
|
|
if !assert.True(t, ok, "expected true") {
|
|
resp := rw.Result()
|
|
defer resp.Body.Close()
|
|
dump, err := httputil.DumpResponse(resp, true)
|
|
require.NoError(t, err)
|
|
t.Log(string(dump))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestReconnectingPTYSignedToken(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dv := coderdtest.DeploymentValues(t)
|
|
dv.Experiments = []string{
|
|
string(codersdk.ExperimentMoons),
|
|
"*",
|
|
}
|
|
|
|
db, pubsub := dbtestutil.NewDB(t)
|
|
client, closer, api, user := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
|
|
Options: &coderdtest.Options{
|
|
DeploymentValues: dv,
|
|
Database: db,
|
|
Pubsub: pubsub,
|
|
IncludeProvisionerDaemon: true,
|
|
},
|
|
LicenseOptions: &coderdenttest.LicenseOptions{
|
|
Features: license.Features{
|
|
codersdk.FeatureWorkspaceProxy: 1,
|
|
},
|
|
},
|
|
})
|
|
t.Cleanup(func() {
|
|
closer.Close()
|
|
})
|
|
|
|
// Create a workspace + apps
|
|
authToken := uuid.NewString()
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
|
|
})
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
|
|
build := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
workspace.LatestBuild = build
|
|
|
|
// Connect an agent to the workspace
|
|
agentID := build.Resources[0].Agents[0].ID
|
|
_ = agenttest.New(t, client.URL, authToken)
|
|
_ = coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
|
|
|
|
proxyURL, err := url.Parse(fmt.Sprintf("https://%s.com", namesgenerator.GetRandomName(1)))
|
|
require.NoError(t, err)
|
|
|
|
_ = coderdenttest.NewWorkspaceProxy(t, api, client, &coderdenttest.ProxyOptions{
|
|
Name: namesgenerator.GetRandomName(1),
|
|
ProxyURL: proxyURL,
|
|
AppHostname: "*.sub.example.com",
|
|
})
|
|
|
|
u, err := url.Parse(proxyURL.String())
|
|
require.NoError(t, err)
|
|
if u.Scheme == "https" {
|
|
u.Scheme = "wss"
|
|
} else {
|
|
u.Scheme = "ws"
|
|
}
|
|
u.Path = fmt.Sprintf("/api/v2/workspaceagents/%s/pty", agentID.String())
|
|
|
|
t.Run("Validate", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
res, err := client.IssueReconnectingPTYSignedToken(ctx, codersdk.IssueReconnectingPTYSignedTokenRequest{
|
|
URL: "",
|
|
AgentID: uuid.Nil,
|
|
})
|
|
require.Error(t, err)
|
|
require.Empty(t, res)
|
|
var sdkErr *codersdk.Error
|
|
require.ErrorAs(t, err, &sdkErr)
|
|
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
|
})
|
|
|
|
t.Run("BadURL", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
res, err := client.IssueReconnectingPTYSignedToken(ctx, codersdk.IssueReconnectingPTYSignedTokenRequest{
|
|
URL: ":",
|
|
AgentID: agentID,
|
|
})
|
|
require.Error(t, err)
|
|
require.Empty(t, res)
|
|
var sdkErr *codersdk.Error
|
|
require.ErrorAs(t, err, &sdkErr)
|
|
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
|
require.Contains(t, sdkErr.Response.Message, "Invalid URL")
|
|
})
|
|
|
|
t.Run("BadURL", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
u := *u
|
|
u.Scheme = "ftp"
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
res, err := client.IssueReconnectingPTYSignedToken(ctx, codersdk.IssueReconnectingPTYSignedTokenRequest{
|
|
URL: u.String(),
|
|
AgentID: agentID,
|
|
})
|
|
require.Error(t, err)
|
|
require.Empty(t, res)
|
|
var sdkErr *codersdk.Error
|
|
require.ErrorAs(t, err, &sdkErr)
|
|
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
|
require.Contains(t, sdkErr.Response.Message, "Invalid URL")
|
|
require.Contains(t, sdkErr.Response.Detail, "scheme")
|
|
})
|
|
|
|
t.Run("BadURLPath", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
u := *u
|
|
u.Path = "/hello"
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
res, err := client.IssueReconnectingPTYSignedToken(ctx, codersdk.IssueReconnectingPTYSignedTokenRequest{
|
|
URL: u.String(),
|
|
AgentID: agentID,
|
|
})
|
|
require.Error(t, err)
|
|
require.Empty(t, res)
|
|
var sdkErr *codersdk.Error
|
|
require.ErrorAs(t, err, &sdkErr)
|
|
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
|
require.Contains(t, sdkErr.Response.Message, "Invalid URL")
|
|
require.Contains(t, sdkErr.Response.Detail, "The provided URL is not a valid reconnecting PTY endpoint URL")
|
|
})
|
|
|
|
t.Run("BadHostname", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
u := *u
|
|
u.Host = "badhostname.com"
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
res, err := client.IssueReconnectingPTYSignedToken(ctx, codersdk.IssueReconnectingPTYSignedTokenRequest{
|
|
URL: u.String(),
|
|
AgentID: agentID,
|
|
})
|
|
require.Error(t, err)
|
|
require.Empty(t, res)
|
|
var sdkErr *codersdk.Error
|
|
require.ErrorAs(t, err, &sdkErr)
|
|
require.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
|
require.Contains(t, sdkErr.Response.Message, "Invalid hostname in URL")
|
|
})
|
|
|
|
t.Run("NoToken", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
unauthedClient := codersdk.New(client.URL)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
res, err := unauthedClient.IssueReconnectingPTYSignedToken(ctx, codersdk.IssueReconnectingPTYSignedTokenRequest{
|
|
URL: u.String(),
|
|
AgentID: agentID,
|
|
})
|
|
require.Error(t, err)
|
|
require.Empty(t, res)
|
|
var sdkErr *codersdk.Error
|
|
require.ErrorAs(t, err, &sdkErr)
|
|
require.Equal(t, http.StatusUnauthorized, sdkErr.StatusCode())
|
|
})
|
|
|
|
t.Run("NoPermissions", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
userClient, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
res, err := userClient.IssueReconnectingPTYSignedToken(ctx, codersdk.IssueReconnectingPTYSignedTokenRequest{
|
|
URL: u.String(),
|
|
AgentID: agentID,
|
|
})
|
|
require.Error(t, err)
|
|
require.Empty(t, res)
|
|
var sdkErr *codersdk.Error
|
|
require.ErrorAs(t, err, &sdkErr)
|
|
require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
|
|
})
|
|
|
|
t.Run("OK", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
res, err := client.IssueReconnectingPTYSignedToken(ctx, codersdk.IssueReconnectingPTYSignedTokenRequest{
|
|
URL: u.String(),
|
|
AgentID: agentID,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, res.SignedToken)
|
|
|
|
// The token is validated in the apptest suite, so we don't need to
|
|
// validate it here.
|
|
})
|
|
}
|