refactor: generate application URL on backend side (#9618)

This commit is contained in:
Marcin Tojek 2023-09-12 15:25:10 +02:00 committed by GitHub
parent 228d1cf361
commit 898971b329
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 346 additions and 79 deletions

4
coderd/apidoc/docs.go generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -41,8 +41,10 @@ type WorkspaceApp struct {
// `coder server` or via a hostname-based dev URL. If this is set to true
// and there is no app wildcard configured on the server, the app will not
// be accessible in the UI.
Subdomain bool `json:"subdomain"`
SharingLevel WorkspaceAppSharingLevel `json:"sharing_level" enums:"owner,authenticated,public"`
Subdomain bool `json:"subdomain"`
// SubdomainName is the application domain exposed on the `coder server`.
SubdomainName string `json:"subdomain_name,omitempty"`
SharingLevel WorkspaceAppSharingLevel `json:"sharing_level" enums:"owner,authenticated,public"`
// Healthcheck specifies the configuration for checking app health.
Healthcheck Healthcheck `json:"healthcheck"`
Health WorkspaceAppHealth `json:"health"`

2
docs/api/agents.md generated
View File

@ -389,6 +389,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/me/manifest \
"sharing_level": "owner",
"slug": "string",
"subdomain": true,
"subdomain_name": "string",
"url": "string"
}
],
@ -658,6 +659,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaceagents/{workspaceagent} \
"sharing_level": "owner",
"slug": "string",
"subdomain": true,
"subdomain_name": "string",
"url": "string"
}
],

8
docs/api/builds.md generated
View File

@ -74,6 +74,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
"sharing_level": "owner",
"slug": "string",
"subdomain": true,
"subdomain_name": "string",
"url": "string"
}
],
@ -237,6 +238,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild} \
"sharing_level": "owner",
"slug": "string",
"subdomain": true,
"subdomain_name": "string",
"url": "string"
}
],
@ -539,6 +541,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/res
"sharing_level": "owner",
"slug": "string",
"subdomain": true,
"subdomain_name": "string",
"url": "string"
}
],
@ -640,6 +643,7 @@ Status Code **200**
| `»»» sharing_level` | [codersdk.WorkspaceAppSharingLevel](schemas.md#codersdkworkspaceappsharinglevel) | false | | |
| `»»» slug` | string | false | | Slug is a unique identifier within the agent. |
| `»»» subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. |
| `»»» subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. |
| `»»» url` | string | false | | URL is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. |
| `»» architecture` | string | false | | |
| `»» connection_timeout_seconds` | integer | false | | |
@ -798,6 +802,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/sta
"sharing_level": "owner",
"slug": "string",
"subdomain": true,
"subdomain_name": "string",
"url": "string"
}
],
@ -966,6 +971,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/builds \
"sharing_level": "owner",
"slug": "string",
"subdomain": true,
"subdomain_name": "string",
"url": "string"
}
],
@ -1103,6 +1109,7 @@ Status Code **200**
| `»»»» sharing_level` | [codersdk.WorkspaceAppSharingLevel](schemas.md#codersdkworkspaceappsharinglevel) | false | | |
| `»»»» slug` | string | false | | Slug is a unique identifier within the agent. |
| `»»»» subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. |
| `»»»» subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. |
| `»»»» url` | string | false | | URL is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. |
| `»»» architecture` | string | false | | |
| `»»» connection_timeout_seconds` | integer | false | | |
@ -1314,6 +1321,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \
"sharing_level": "owner",
"slug": "string",
"subdomain": true,
"subdomain_name": "string",
"url": "string"
}
],

34
docs/api/schemas.md generated
View File

@ -198,6 +198,7 @@
"sharing_level": "owner",
"slug": "string",
"subdomain": true,
"subdomain_name": "string",
"url": "string"
}
],
@ -5438,6 +5439,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"sharing_level": "owner",
"slug": "string",
"subdomain": true,
"subdomain_name": "string",
"url": "string"
}
],
@ -5581,6 +5583,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"sharing_level": "owner",
"slug": "string",
"subdomain": true,
"subdomain_name": "string",
"url": "string"
}
],
@ -5939,25 +5942,27 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"sharing_level": "owner",
"slug": "string",
"subdomain": true,
"subdomain_name": "string",
"url": "string"
}
```
### Properties
| Name | Type | Required | Restrictions | Description |
| --------------- | ---------------------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `command` | string | false | | |
| `display_name` | string | false | | Display name is a friendly name for the app. |
| `external` | boolean | false | | External specifies whether the URL should be opened externally on the client or not. |
| `health` | [codersdk.WorkspaceAppHealth](#codersdkworkspaceapphealth) | false | | |
| `healthcheck` | [codersdk.Healthcheck](#codersdkhealthcheck) | false | | Healthcheck specifies the configuration for checking app health. |
| `icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. |
| `id` | string | false | | |
| `sharing_level` | [codersdk.WorkspaceAppSharingLevel](#codersdkworkspaceappsharinglevel) | false | | |
| `slug` | string | false | | Slug is a unique identifier within the agent. |
| `subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. |
| `url` | string | false | | URL is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. |
| Name | Type | Required | Restrictions | Description |
| ---------------- | ---------------------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `command` | string | false | | |
| `display_name` | string | false | | Display name is a friendly name for the app. |
| `external` | boolean | false | | External specifies whether the URL should be opened externally on the client or not. |
| `health` | [codersdk.WorkspaceAppHealth](#codersdkworkspaceapphealth) | false | | |
| `healthcheck` | [codersdk.Healthcheck](#codersdkhealthcheck) | false | | Healthcheck specifies the configuration for checking app health. |
| `icon` | string | false | | Icon is a relative path or external URL that specifies an icon to be displayed in the dashboard. |
| `id` | string | false | | |
| `sharing_level` | [codersdk.WorkspaceAppSharingLevel](#codersdkworkspaceappsharinglevel) | false | | |
| `slug` | string | false | | Slug is a unique identifier within the agent. |
| `subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. |
| `subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. |
| `url` | string | false | | URL is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. |
#### Enumerated Values
@ -6051,6 +6056,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"sharing_level": "owner",
"slug": "string",
"subdomain": true,
"subdomain_name": "string",
"url": "string"
}
],
@ -6363,6 +6369,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"sharing_level": "owner",
"slug": "string",
"subdomain": true,
"subdomain_name": "string",
"url": "string"
}
],
@ -6577,6 +6584,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
"sharing_level": "owner",
"slug": "string",
"subdomain": true,
"subdomain_name": "string",
"url": "string"
}
],

4
docs/api/templates.md generated
View File

@ -1585,6 +1585,7 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/d
"sharing_level": "owner",
"slug": "string",
"subdomain": true,
"subdomain_name": "string",
"url": "string"
}
],
@ -1686,6 +1687,7 @@ Status Code **200**
| `»»» sharing_level` | [codersdk.WorkspaceAppSharingLevel](schemas.md#codersdkworkspaceappsharinglevel) | false | | |
| `»»» slug` | string | false | | Slug is a unique identifier within the agent. |
| `»»» subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. |
| `»»» subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. |
| `»»» url` | string | false | | URL is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. |
| `»» architecture` | string | false | | |
| `»» connection_timeout_seconds` | integer | false | | |
@ -1978,6 +1980,7 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/r
"sharing_level": "owner",
"slug": "string",
"subdomain": true,
"subdomain_name": "string",
"url": "string"
}
],
@ -2079,6 +2082,7 @@ Status Code **200**
| `»»» sharing_level` | [codersdk.WorkspaceAppSharingLevel](schemas.md#codersdkworkspaceappsharinglevel) | false | | |
| `»»» slug` | string | false | | Slug is a unique identifier within the agent. |
| `»»» subdomain` | boolean | false | | Subdomain denotes whether the app should be accessed via a path on the `coder server` or via a hostname-based dev URL. If this is set to true and there is no app wildcard configured on the server, the app will not be accessible in the UI. |
| `»»» subdomain_name` | string | false | | Subdomain name is the application domain exposed on the `coder server`. |
| `»»» url` | string | false | | URL is the address being proxied to inside the workspace. If external is specified, this will be opened on the client. |
| `»» architecture` | string | false | | |
| `»» connection_timeout_seconds` | integer | false | | |

View File

@ -104,6 +104,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/member
"sharing_level": "owner",
"slug": "string",
"subdomain": true,
"subdomain_name": "string",
"url": "string"
}
],
@ -294,6 +295,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam
"sharing_level": "owner",
"slug": "string",
"subdomain": true,
"subdomain_name": "string",
"url": "string"
}
],
@ -483,6 +485,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \
"sharing_level": "owner",
"slug": "string",
"subdomain": true,
"subdomain_name": "string",
"url": "string"
}
],
@ -674,6 +677,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \
"sharing_level": "owner",
"slug": "string",
"subdomain": true,
"subdomain_name": "string",
"url": "string"
}
],
@ -944,6 +948,7 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \
"sharing_level": "owner",
"slug": "string",
"subdomain": true,
"subdomain_name": "string",
"url": "string"
}
],

View File

@ -1416,6 +1416,7 @@ export interface WorkspaceApp {
readonly command?: string;
readonly icon?: string;
readonly subdomain: boolean;
readonly subdomain_name?: string;
readonly sharing_level: WorkspaceAppSharingLevel;
readonly healthcheck: Healthcheck;
readonly health: WorkspaceAppHealth;

View File

@ -18,10 +18,13 @@ const meta: Meta<typeof AppLink> = {
<ProxyContext.Provider
value={{
proxyLatencies: MockProxyLatencies,
proxy: getPreferredProxy(
MockWorkspaceProxies,
MockPrimaryWorkspaceProxy,
),
proxy: {
...getPreferredProxy(
MockWorkspaceProxies,
MockPrimaryWorkspaceProxy,
),
preferredWildcardHostname: "*.super_proxy.tld",
},
proxies: MockWorkspaceProxies,
isLoading: false,
isFetched: true,
@ -135,3 +138,16 @@ export const HealthUnhealthy: Story = {
agent: MockWorkspaceAgent,
},
};
export const InternalApp: Story = {
args: {
workspace: MockWorkspace,
app: {
...MockWorkspaceApp,
display_name: "Check my URL",
subdomain: true,
subdomain_name: "slug--agent_name--workspace_name--username",
},
agent: MockWorkspaceAgent,
},
};

View File

@ -11,6 +11,7 @@ import { generateRandomString } from "../../../utils/random";
import { BaseIcon } from "./BaseIcon";
import { ShareIcon } from "./ShareIcon";
import { useProxy } from "contexts/ProxyContext";
import { createAppLinkHref } from "utils/apps";
const Language = {
appTitle: (appName: string, identifier: string): string =>
@ -40,24 +41,16 @@ export const AppLink: FC<AppLinkProps> = ({ app, workspace, agent }) => {
appDisplayName = appSlug;
}
// The backend redirects if the trailing slash isn't included, so we add it
// here to avoid extra roundtrips.
let href = `${preferredPathBase}/@${username}/${workspace.name}.${
agent.name
}/apps/${encodeURIComponent(appSlug)}/`;
if (app.command) {
href = `${preferredPathBase}/@${username}/${workspace.name}.${
agent.name
}/terminal?command=${encodeURIComponent(app.command)}`;
}
if (appsHost && app.subdomain) {
const subdomain = `${appSlug}--${agent.name}--${workspace.name}--${username}`;
href = `${window.location.protocol}//${appsHost}/`.replace("*", subdomain);
}
if (app.external) {
href = app.url;
}
const href = createAppLinkHref(
window.location.protocol,
preferredPathBase,
appsHost,
appSlug,
username,
workspace,
agent,
app,
);
let canClick = true;
let icon = <BaseIcon app={app} />;

103
site/src/utils/apps.test.ts Normal file
View File

@ -0,0 +1,103 @@
import { createAppLinkHref } from "./apps";
import {
MockWorkspace,
MockWorkspaceAgent,
MockWorkspaceApp,
} from "testHelpers/entities";
describe("create app link", () => {
it("with external URL", () => {
const externalURL = "https://external-url.tld";
const href = createAppLinkHref(
"http:",
"/path-base",
"*.apps-host.tld",
"app-slug",
"username",
MockWorkspace,
MockWorkspaceAgent,
{
...MockWorkspaceApp,
external: true,
url: externalURL,
},
);
expect(href).toBe(externalURL);
});
it("without subdomain", () => {
const href = createAppLinkHref(
"http:",
"/path-base",
"*.apps-host.tld",
"app-slug",
"username",
MockWorkspace,
MockWorkspaceAgent,
{
...MockWorkspaceApp,
subdomain: false,
},
);
expect(href).toBe(
"/path-base/@username/Test-Workspace.a-workspace-agent/apps/app-slug/",
);
});
it("with command", () => {
const href = createAppLinkHref(
"https:",
"/path-base",
"*.apps-host.tld",
"app-slug",
"username",
MockWorkspace,
MockWorkspaceAgent,
{
...MockWorkspaceApp,
command: "ls -la",
},
);
expect(href).toBe(
"/path-base/@username/Test-Workspace.a-workspace-agent/terminal?command=ls%20-la",
);
});
it("with subdomain", () => {
const href = createAppLinkHref(
"ftps:",
"/path-base",
"*.apps-host.tld",
"app-slug",
"username",
MockWorkspace,
MockWorkspaceAgent,
{
...MockWorkspaceApp,
subdomain: true,
subdomain_name: "hellocoder",
},
);
expect(href).toBe("ftps://hellocoder.apps-host.tld/");
});
it("with subdomain, but not apps host", () => {
const href = createAppLinkHref(
"ftps:",
"/path-base",
"",
"app-slug",
"username",
MockWorkspace,
MockWorkspaceAgent,
{
...MockWorkspaceApp,
subdomain: true,
subdomain_name: "hellocoder",
},
);
expect(href).toBe(
"/path-base/@username/Test-Workspace.a-workspace-agent/apps/app-slug/",
);
});
});

32
site/src/utils/apps.ts Normal file
View File

@ -0,0 +1,32 @@
import * as TypesGen from "../api/typesGenerated";
export const createAppLinkHref = (
protocol: string,
preferredPathBase: string,
appsHost: string,
appSlug: string,
username: string,
workspace: TypesGen.Workspace,
agent: TypesGen.WorkspaceAgent,
app: TypesGen.WorkspaceApp,
): string => {
if (app.external) {
return app.url;
}
// The backend redirects if the trailing slash isn't included, so we add it
// here to avoid extra roundtrips.
let href = `${preferredPathBase}/@${username}/${workspace.name}.${
agent.name
}/apps/${encodeURIComponent(appSlug)}/`;
if (app.command) {
href = `${preferredPathBase}/@${username}/${workspace.name}.${
agent.name
}/terminal?command=${encodeURIComponent(app.command)}`;
}
if (appsHost && app.subdomain && app.subdomain_name) {
href = `${protocol}//${appsHost}/`.replace("*", app.subdomain_name);
}
return href;
};