mirror of
https://github.com/coder/coder.git
synced 2025-07-08 11:39:50 +00:00
refactor: generate application URL on backend side (#9618)
This commit is contained in:
4
coderd/apidoc/docs.go
generated
4
coderd/apidoc/docs.go
generated
@ -10970,6 +10970,10 @@ const docTemplate = `{
|
||||
"description": "Subdomain denotes whether the app should be accessed via a path on the\n` + "`" + `coder server` + "`" + ` or via a hostname-based dev URL. If this is set to true\nand there is no app wildcard configured on the server, the app will not\nbe accessible in the UI.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"subdomain_name": {
|
||||
"description": "SubdomainName is the application domain exposed on the ` + "`" + `coder server` + "`" + `.",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"description": "URL is the address being proxied to inside the workspace.\nIf external is specified, this will be opened on the client.",
|
||||
"type": "string"
|
||||
|
4
coderd/apidoc/swagger.json
generated
4
coderd/apidoc/swagger.json
generated
@ -9961,6 +9961,10 @@
|
||||
"description": "Subdomain denotes whether the app should be accessed via a path on the\n`coder server` or via a hostname-based dev URL. If this is set to true\nand there is no app wildcard configured on the server, the app will not\nbe accessible in the UI.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"subdomain_name": {
|
||||
"description": "SubdomainName is the application domain exposed on the `coder server`.",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"description": "URL is the address being proxied to inside the workspace.\nIf external is specified, this will be opened on the client.",
|
||||
"type": "string"
|
||||
|
@ -149,7 +149,7 @@ func (api *API) provisionerJobResources(rw http.ResponseWriter, r *http.Request,
|
||||
}
|
||||
|
||||
apiAgent, err := convertWorkspaceAgent(
|
||||
api.DERPMap(), *api.TailnetCoordinator.Load(), agent, convertApps(dbApps), api.AgentInactiveDisconnectTimeout,
|
||||
api.DERPMap(), *api.TailnetCoordinator.Load(), agent, convertProvisionedApps(dbApps), api.AgentInactiveDisconnectTimeout,
|
||||
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -64,8 +64,42 @@ func (api *API) workspaceAgent(rw http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
resource, err := api.Database.GetWorkspaceResourceByID(ctx, workspaceAgent.ResourceID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching workspace resource.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
build, err := api.Database.GetWorkspaceBuildByJobID(ctx, resource.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching workspace build.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
workspace, err := api.Database.GetWorkspaceByID(ctx, build.WorkspaceID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching workspace.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
owner, err := api.Database.GetUserByID(ctx, workspace.OwnerID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching workspace owner.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
apiAgent, err := convertWorkspaceAgent(
|
||||
api.DERPMap(), *api.TailnetCoordinator.Load(), workspaceAgent, convertApps(dbApps), api.AgentInactiveDisconnectTimeout,
|
||||
api.DERPMap(), *api.TailnetCoordinator.Load(), workspaceAgent, convertApps(dbApps, workspaceAgent, owner, workspace), api.AgentInactiveDisconnectTimeout,
|
||||
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
|
||||
)
|
||||
if err != nil {
|
||||
@ -165,7 +199,7 @@ func (api *API) workspaceAgentManifest(rw http.ResponseWriter, r *http.Request)
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, agentsdk.Manifest{
|
||||
AgentID: apiAgent.ID,
|
||||
Apps: convertApps(dbApps),
|
||||
Apps: convertApps(dbApps, workspaceAgent, owner, workspace),
|
||||
DERPMap: api.DERPMap(),
|
||||
DERPForceWebSockets: api.DeploymentValues.DERP.Config.ForceWebSockets.Value(),
|
||||
GitAuthConfigs: len(api.GitAuthConfigs),
|
||||
@ -1281,19 +1315,40 @@ func (api *API) workspaceAgentClientCoordinate(rw http.ResponseWriter, r *http.R
|
||||
}
|
||||
}
|
||||
|
||||
func convertApps(dbApps []database.WorkspaceApp) []codersdk.WorkspaceApp {
|
||||
// convertProvisionedApps converts applications that are in the middle of provisioning process.
|
||||
// It means that they may not have an agent or workspace assigned (dry-run job).
|
||||
func convertProvisionedApps(dbApps []database.WorkspaceApp) []codersdk.WorkspaceApp {
|
||||
return convertApps(dbApps, database.WorkspaceAgent{}, database.User{}, database.Workspace{})
|
||||
}
|
||||
|
||||
func convertApps(dbApps []database.WorkspaceApp, agent database.WorkspaceAgent, owner database.User, workspace database.Workspace) []codersdk.WorkspaceApp {
|
||||
apps := make([]codersdk.WorkspaceApp, 0)
|
||||
for _, dbApp := range dbApps {
|
||||
var subdomainName string
|
||||
if dbApp.Subdomain && agent.Name != "" && owner.Username != "" && workspace.Name != "" {
|
||||
appSlug := dbApp.Slug
|
||||
if appSlug == "" {
|
||||
appSlug = dbApp.DisplayName
|
||||
}
|
||||
subdomainName = httpapi.ApplicationURL{
|
||||
AppSlugOrPort: appSlug,
|
||||
AgentName: agent.Name,
|
||||
WorkspaceName: workspace.Name,
|
||||
Username: owner.Username,
|
||||
}.String()
|
||||
}
|
||||
|
||||
apps = append(apps, codersdk.WorkspaceApp{
|
||||
ID: dbApp.ID,
|
||||
URL: dbApp.Url.String,
|
||||
External: dbApp.External,
|
||||
Slug: dbApp.Slug,
|
||||
DisplayName: dbApp.DisplayName,
|
||||
Command: dbApp.Command.String,
|
||||
Icon: dbApp.Icon,
|
||||
Subdomain: dbApp.Subdomain,
|
||||
SharingLevel: codersdk.WorkspaceAppSharingLevel(dbApp.SharingLevel),
|
||||
ID: dbApp.ID,
|
||||
URL: dbApp.Url.String,
|
||||
External: dbApp.External,
|
||||
Slug: dbApp.Slug,
|
||||
DisplayName: dbApp.DisplayName,
|
||||
Command: dbApp.Command.String,
|
||||
Icon: dbApp.Icon,
|
||||
Subdomain: dbApp.Subdomain,
|
||||
SubdomainName: subdomainName,
|
||||
SharingLevel: codersdk.WorkspaceAppSharingLevel(dbApp.SharingLevel),
|
||||
Healthcheck: codersdk.Healthcheck{
|
||||
URL: dbApp.HealthcheckUrl,
|
||||
Interval: dbApp.HealthcheckInterval,
|
||||
|
@ -291,6 +291,37 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
|
||||
}
|
||||
|
||||
appURL := fmt.Sprintf("%s://127.0.0.1:%d?%s", scheme, port, proxyTestAppQuery)
|
||||
protoApps := []*proto.App{
|
||||
{
|
||||
Slug: proxyTestAppNameFake,
|
||||
DisplayName: proxyTestAppNameFake,
|
||||
SharingLevel: proto.AppSharingLevel_OWNER,
|
||||
// Hopefully this IP and port doesn't exist.
|
||||
Url: "http://127.1.0.1:65535",
|
||||
Subdomain: true,
|
||||
},
|
||||
{
|
||||
Slug: proxyTestAppNameOwner,
|
||||
DisplayName: proxyTestAppNameOwner,
|
||||
SharingLevel: proto.AppSharingLevel_OWNER,
|
||||
Url: appURL,
|
||||
Subdomain: true,
|
||||
},
|
||||
{
|
||||
Slug: proxyTestAppNameAuthenticated,
|
||||
DisplayName: proxyTestAppNameAuthenticated,
|
||||
SharingLevel: proto.AppSharingLevel_AUTHENTICATED,
|
||||
Url: appURL,
|
||||
Subdomain: true,
|
||||
},
|
||||
{
|
||||
Slug: proxyTestAppNamePublic,
|
||||
DisplayName: proxyTestAppNamePublic,
|
||||
SharingLevel: proto.AppSharingLevel_PUBLIC,
|
||||
Url: appURL,
|
||||
Subdomain: true,
|
||||
},
|
||||
}
|
||||
version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: echo.PlanComplete,
|
||||
@ -306,33 +337,7 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
|
||||
Auth: &proto.Agent_Token{
|
||||
Token: authToken,
|
||||
},
|
||||
Apps: []*proto.App{
|
||||
{
|
||||
Slug: proxyTestAppNameFake,
|
||||
DisplayName: proxyTestAppNameFake,
|
||||
SharingLevel: proto.AppSharingLevel_OWNER,
|
||||
// Hopefully this IP and port doesn't exist.
|
||||
Url: "http://127.1.0.1:65535",
|
||||
},
|
||||
{
|
||||
Slug: proxyTestAppNameOwner,
|
||||
DisplayName: proxyTestAppNameOwner,
|
||||
SharingLevel: proto.AppSharingLevel_OWNER,
|
||||
Url: appURL,
|
||||
},
|
||||
{
|
||||
Slug: proxyTestAppNameAuthenticated,
|
||||
DisplayName: proxyTestAppNameAuthenticated,
|
||||
SharingLevel: proto.AppSharingLevel_AUTHENTICATED,
|
||||
Url: appURL,
|
||||
},
|
||||
{
|
||||
Slug: proxyTestAppNamePublic,
|
||||
DisplayName: proxyTestAppNamePublic,
|
||||
SharingLevel: proto.AppSharingLevel_PUBLIC,
|
||||
Url: appURL,
|
||||
},
|
||||
},
|
||||
Apps: protoApps,
|
||||
}},
|
||||
}},
|
||||
},
|
||||
@ -342,7 +347,22 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
|
||||
template := coderdtest.CreateTemplate(t, client, orgID, version.ID)
|
||||
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
|
||||
workspace := coderdtest.CreateWorkspace(t, client, orgID, template.ID, workspaceMutators...)
|
||||
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
workspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
// Verify app subdomains
|
||||
for _, app := range workspaceBuild.Resources[0].Agents[0].Apps {
|
||||
require.True(t, app.Subdomain)
|
||||
|
||||
appURL := httpapi.ApplicationURL{
|
||||
// findProtoApp is needed as the order of apps returned from PG database
|
||||
// is not guaranteed.
|
||||
AppSlugOrPort: findProtoApp(t, protoApps, app.Slug).Slug,
|
||||
AgentName: proxyTestAgentName,
|
||||
WorkspaceName: workspace.Name,
|
||||
Username: me.Username,
|
||||
}
|
||||
require.Equal(t, appURL.String(), app.SubdomainName)
|
||||
}
|
||||
|
||||
agentClient := agentsdk.New(client.URL)
|
||||
agentClient.SetSessionToken(authToken)
|
||||
@ -388,6 +408,16 @@ func createWorkspaceWithApps(t *testing.T, client *codersdk.Client, orgID uuid.U
|
||||
return workspace, agents[0]
|
||||
}
|
||||
|
||||
func findProtoApp(t *testing.T, protoApps []*proto.App, slug string) *proto.App {
|
||||
for _, protoApp := range protoApps {
|
||||
if protoApp.Slug == slug {
|
||||
return protoApp
|
||||
}
|
||||
}
|
||||
require.FailNowf(t, "proto app not found (slug: %q)", slug)
|
||||
return nil
|
||||
}
|
||||
|
||||
func doWithRetries(t require.TestingT, client *codersdk.Client, req *http.Request) (*http.Response, error) {
|
||||
var resp *http.Response
|
||||
var err error
|
||||
|
@ -832,7 +832,7 @@ func (api *API) convertWorkspaceBuild(
|
||||
for _, agent := range agents {
|
||||
apps := appsByAgentID[agent.ID]
|
||||
apiAgent, err := convertWorkspaceAgent(
|
||||
api.DERPMap(), *api.TailnetCoordinator.Load(), agent, convertApps(apps), api.AgentInactiveDisconnectTimeout,
|
||||
api.DERPMap(), *api.TailnetCoordinator.Load(), agent, convertApps(apps, agent, owner, workspace), api.AgentInactiveDisconnectTimeout,
|
||||
api.DeploymentValues.AgentFallbackTroubleshootingURL.String(),
|
||||
)
|
||||
if err != nil {
|
||||
|
Reference in New Issue
Block a user