Skip to content

Commit 898971b

Browse files
authored
refactor: generate application URL on backend side (coder#9618)
1 parent 228d1cf commit 898971b

File tree

17 files changed

+346
-79
lines changed

17 files changed

+346
-79
lines changed

coderd/apidoc/docs.go

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/provisionerjobs.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ func (api *API) provisionerJobResources(rw http.ResponseWriter, r *http.Request,
149149
}
150150

151151
apiAgent, err := convertWorkspaceAgent(
152-
api.DERPMap(), *api.TailnetCoordinator.Load(), agent, convertApps(dbApps), api.AgentInactiveDisconnectTimeout,
152+
api.DERPMap(), *api.TailnetCoordinator.Load(), agent, convertProvisionedApps(dbApps), api.AgentInactiveDisconnectTimeout,
153153
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
154154
)
155155
if err != nil {

coderd/workspaceagents.go

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,42 @@ func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) {
6464
})
6565
return
6666
}
67+
68+
resource, err := api.Database.GetWorkspaceResourceByID(ctx, workspaceAgent.ResourceID)
69+
if err != nil {
70+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
71+
Message: "Internal error fetching workspace resource.",
72+
Detail: err.Error(),
73+
})
74+
return
75+
}
76+
build, err := api.Database.GetWorkspaceBuildByJobID(ctx, resource.JobID)
77+
if err != nil {
78+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
79+
Message: "Internal error fetching workspace build.",
80+
Detail: err.Error(),
81+
})
82+
return
83+
}
84+
workspace, err := api.Database.GetWorkspaceByID(ctx, build.WorkspaceID)
85+
if err != nil {
86+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
87+
Message: "Internal error fetching workspace.",
88+
Detail: err.Error(),
89+
})
90+
return
91+
}
92+
owner, err := api.Database.GetUserByID(ctx, workspace.OwnerID)
93+
if err != nil {
94+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
95+
Message: "Internal error fetching workspace owner.",
96+
Detail: err.Error(),
97+
})
98+
return
99+
}
100+
67101
apiAgent, err := convertWorkspaceAgent(
68-
api.DERPMap(), *api.TailnetCoordinator.Load(), workspaceAgent, convertApps(dbApps), api.AgentInactiveDisconnectTimeout,
102+
api.DERPMap(), *api.TailnetCoordinator.Load(), workspaceAgent, convertApps(dbApps, workspaceAgent, owner, workspace), api.AgentInactiveDisconnectTimeout,
69103
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
70104
)
71105
if err != nil {
@@ -165,7 +199,7 @@ func (api *API) workspaceAgentManifest(rw http.ResponseWriter, r *http.Request)
165199

166200
httpapi.Write(ctx, rw, http.StatusOK, agentsdk.Manifest{
167201
AgentID: apiAgent.ID,
168-
Apps: convertApps(dbApps),
202+
Apps: convertApps(dbApps, workspaceAgent, owner, workspace),
169203
DERPMap: api.DERPMap(),
170204
DERPForceWebSockets: api.DeploymentValues.DERP.Config.ForceWebSockets.Value(),
171205
GitAuthConfigs: len(api.GitAuthConfigs),
@@ -1281,19 +1315,40 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R
12811315
}
12821316
}
12831317

1284-
func convertApps(dbApps []database.WorkspaceApp) []codersdk.WorkspaceApp {
1318+
// convertProvisionedApps converts applications that are in the middle of provisioning process.
1319+
// It means that they may not have an agent or workspace assigned (dry-run job).
1320+
func convertProvisionedApps(dbApps []database.WorkspaceApp) []codersdk.WorkspaceApp {
1321+
return convertApps(dbApps, database.WorkspaceAgent{}, database.User{}, database.Workspace{})
1322+
}
1323+
1324+
func convertApps(dbApps []database.WorkspaceApp, agent database.WorkspaceAgent, owner database.User, workspace database.Workspace) []codersdk.WorkspaceApp {
12851325
apps := make([]codersdk.WorkspaceApp, 0)
12861326
for _, dbApp := range dbApps {
1327+
var subdomainName string
1328+
if dbApp.Subdomain && agent.Name != "" && owner.Username != "" && workspace.Name != "" {
1329+
appSlug := dbApp.Slug
1330+
if appSlug == "" {
1331+
appSlug = dbApp.DisplayName
1332+
}
1333+
subdomainName = httpapi.ApplicationURL{
1334+
AppSlugOrPort: appSlug,
1335+
AgentName: agent.Name,
1336+
WorkspaceName: workspace.Name,
1337+
Username: owner.Username,
1338+
}.String()
1339+
}
1340+
12871341
apps = append(apps, codersdk.WorkspaceApp{
1288-
ID: dbApp.ID,
1289-
URL: dbApp.Url.String,
1290-
External: dbApp.External,
1291-
Slug: dbApp.Slug,
1292-
DisplayName: dbApp.DisplayName,
1293-
Command: dbApp.Command.String,
1294-
Icon: dbApp.Icon,
1295-
Subdomain: dbApp.Subdomain,
1296-
SharingLevel: codersdk.WorkspaceAppSharingLevel(dbApp.SharingLevel),
1342+
ID: dbApp.ID,
1343+
URL: dbApp.Url.String,
1344+
External: dbApp.External,
1345+
Slug: dbApp.Slug,
1346+
DisplayName: dbApp.DisplayName,
1347+
Command: dbApp.Command.String,
1348+
Icon: dbApp.Icon,
1349+
Subdomain: dbApp.Subdomain,
1350+
SubdomainName: subdomainName,
1351+
SharingLevel: codersdk.WorkspaceAppSharingLevel(dbApp.SharingLevel),
12971352
Healthcheck: codersdk.Healthcheck{
12981353
URL: dbApp.HealthcheckUrl,
12991354
Interval: dbApp.HealthcheckInterval,

coderd/workspaceapps/apptest/setup.go

Lines changed: 58 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,37 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
291291
}
292292

293293
appURL := fmt.Sprintf("%s://127.0.0.1:%d?%s", scheme, port, proxyTestAppQuery)
294+
protoApps := []*proto.App{
295+
{
296+
Slug: proxyTestAppNameFake,
297+
DisplayName: proxyTestAppNameFake,
298+
SharingLevel: proto.AppSharingLevel_OWNER,
299+
// Hopefully this IP and port doesn't exist.
300+
Url: "http://127.1.0.1:65535",
301+
Subdomain: true,
302+
},
303+
{
304+
Slug: proxyTestAppNameOwner,
305+
DisplayName: proxyTestAppNameOwner,
306+
SharingLevel: proto.AppSharingLevel_OWNER,
307+
Url: appURL,
308+
Subdomain: true,
309+
},
310+
{
311+
Slug: proxyTestAppNameAuthenticated,
312+
DisplayName: proxyTestAppNameAuthenticated,
313+
SharingLevel: proto.AppSharingLevel_AUTHENTICATED,
314+
Url: appURL,
315+
Subdomain: true,
316+
},
317+
{
318+
Slug: proxyTestAppNamePublic,
319+
DisplayName: proxyTestAppNamePublic,
320+
SharingLevel: proto.AppSharingLevel_PUBLIC,
321+
Url: appURL,
322+
Subdomain: true,
323+
},
324+
}
294325
version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{
295326
Parse: echo.ParseComplete,
296327
ProvisionPlan: echo.PlanComplete,
@@ -306,33 +337,7 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
306337
Auth: &proto.Agent_Token{
307338
Token: authToken,
308339
},
309-
Apps: []*proto.App{
310-
{
311-
Slug: proxyTestAppNameFake,
312-
DisplayName: proxyTestAppNameFake,
313-
SharingLevel: proto.AppSharingLevel_OWNER,
314-
// Hopefully this IP and port doesn't exist.
315-
Url: "http://127.1.0.1:65535",
316-
},
317-
{
318-
Slug: proxyTestAppNameOwner,
319-
DisplayName: proxyTestAppNameOwner,
320-
SharingLevel: proto.AppSharingLevel_OWNER,
321-
Url: appURL,
322-
},
323-
{
324-
Slug: proxyTestAppNameAuthenticated,
325-
DisplayName: proxyTestAppNameAuthenticated,
326-
SharingLevel: proto.AppSharingLevel_AUTHENTICATED,
327-
Url: appURL,
328-
},
329-
{
330-
Slug: proxyTestAppNamePublic,
331-
DisplayName: proxyTestAppNamePublic,
332-
SharingLevel: proto.AppSharingLevel_PUBLIC,
333-
Url: appURL,
334-
},
335-
},
340+
Apps: protoApps,
336341
}},
337342
}},
338343
},
@@ -342,7 +347,22 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
342347
template := coderdtest.CreateTemplate(t, client, orgID, version.ID)
343348
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
344349
workspace := coderdtest.CreateWorkspace(t, client, orgID, template.ID, workspaceMutators...)
345-
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
350+
workspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
351+
352+
// Verify app subdomains
353+
for _, app := range workspaceBuild.Resources[0].Agents[0].Apps {
354+
require.True(t, app.Subdomain)
355+
356+
appURL := httpapi.ApplicationURL{
357+
// findProtoApp is needed as the order of apps returned from PG database
358+
// is not guaranteed.
359+
AppSlugOrPort: findProtoApp(t, protoApps, app.Slug).Slug,
360+
AgentName: proxyTestAgentName,
361+
WorkspaceName: workspace.Name,
362+
Username: me.Username,
363+
}
364+
require.Equal(t, appURL.String(), app.SubdomainName)
365+
}
346366

347367
agentClient := agentsdk.New(client.URL)
348368
agentClient.SetSessionToken(authToken)
@@ -388,6 +408,16 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
388408
return workspace, agents[0]
389409
}
390410

411+
func findProtoApp(t *testing.T, protoApps []*proto.App, slug string) *proto.App {
412+
for _, protoApp := range protoApps {
413+
if protoApp.Slug == slug {
414+
return protoApp
415+
}
416+
}
417+
require.FailNowf(t, "proto app not found (slug: %q)", slug)
418+
return nil
419+
}
420+
391421
func doWithRetries(t require.TestingT, client *codersdk.Client, req *http.Request) (*http.Response, error) {
392422
var resp *http.Response
393423
var err error

coderd/workspacebuilds.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -832,7 +832,7 @@ func (api *API) convertWorkspaceBuild(
832832
for _, agent := range agents {
833833
apps := appsByAgentID[agent.ID]
834834
apiAgent, err := convertWorkspaceAgent(
835-
api.DERPMap(), *api.TailnetCoordinator.Load(), agent, convertApps(apps), api.AgentInactiveDisconnectTimeout,
835+
api.DERPMap(), *api.TailnetCoordinator.Load(), agent, convertApps(apps, agent, owner, workspace), api.AgentInactiveDisconnectTimeout,
836836
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
837837
)
838838
if err != nil {

codersdk/workspaceapps.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@ type WorkspaceApp struct {
4141
// `coder server` or via a hostname-based dev URL. If this is set to true
4242
// and there is no app wildcard configured on the server, the app will not
4343
// be accessible in the UI.
44-
Subdomain bool `json:"subdomain"`
45-
SharingLevel WorkspaceAppSharingLevel `json:"sharing_level" enums:"owner,authenticated,public"`
44+
Subdomain bool `json:"subdomain"`
45+
// SubdomainName is the application domain exposed on the `coder server`.
46+
SubdomainName string `json:"subdomain_name,omitempty"`
47+
SharingLevel WorkspaceAppSharingLevel `json:"sharing_level" enums:"owner,authenticated,public"`
4648
// Healthcheck specifies the configuration for checking app health.
4749
Healthcheck Healthcheck `json:"healthcheck"`
4850
Health WorkspaceAppHealth `json:"health"`

docs/api/agents.md

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/api/builds.md

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)