mirror of https://github.com/coder/coder.git
refactor: generate application URL on backend side (#9618)
This commit is contained in:
parent
228d1cf361
commit
898971b329
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
],
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
],
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
],
|
||||
|
|
|
@ -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 | | |
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
],
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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} />;
|
||||
|
|
|
@ -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/",
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
};
|
Loading…
Reference in New Issue