feat: remove agent name from app URLs (#19750)

## Summary

In this pull request we're removing `agent_name` from subdomains in APP
urls when an `app` is used in the subdomain. `agent_names` will still be
used when a `port` is used in the subdomain.

Closes: https://github.com/coder/coder/issues/18485

### Changes

- Updated regex to support an optional agent name
- Added logic to support checking the app slug for a matching port
(e.g., 8080 or 8080s)

### Testing

- Updated all tests to support an optional `agent_name`
This commit is contained in:
Rafael Rodriguez
2025-09-26 12:25:58 -05:00
committed by GitHub
parent 403a9e57c9
commit d29a52462b
6 changed files with 359 additions and 99 deletions
+11 -5
View File
@@ -110,7 +110,7 @@ func (a *ManifestAPI) GetManifest(ctx context.Context, _ *agentproto.GetManifest
}
}
apps, err := dbAppsToProto(dbApps, workspaceAgent, workspace.OwnerUsername, workspace)
apps, err := dbAppsToProto(dbApps, workspaceAgent, workspace.OwnerUsername, workspace, a.AppHostname)
if err != nil {
return nil, xerrors.Errorf("converting workspace apps: %w", err)
}
@@ -196,11 +196,11 @@ func dbAgentScriptToProto(script database.WorkspaceAgentScript) *agentproto.Work
}
}
func dbAppsToProto(dbApps []database.WorkspaceApp, agent database.WorkspaceAgent, ownerName string, workspace database.Workspace) ([]*agentproto.WorkspaceApp, error) {
func dbAppsToProto(dbApps []database.WorkspaceApp, agent database.WorkspaceAgent, ownerName string, workspace database.Workspace, appHostname string) ([]*agentproto.WorkspaceApp, error) {
ret := make([]*agentproto.WorkspaceApp, len(dbApps))
for i, dbApp := range dbApps {
var err error
ret[i], err = dbAppToProto(dbApp, agent, ownerName, workspace)
ret[i], err = dbAppToProto(dbApp, agent, ownerName, workspace, appHostname)
if err != nil {
return nil, xerrors.Errorf("parse app %v (%q): %w", i, dbApp.Slug, err)
}
@@ -208,7 +208,7 @@ func dbAppsToProto(dbApps []database.WorkspaceApp, agent database.WorkspaceAgent
return ret, nil
}
func dbAppToProto(dbApp database.WorkspaceApp, agent database.WorkspaceAgent, ownerName string, workspace database.Workspace) (*agentproto.WorkspaceApp, error) {
func dbAppToProto(dbApp database.WorkspaceApp, agent database.WorkspaceAgent, ownerName string, workspace database.Workspace, appHostname string) (*agentproto.WorkspaceApp, error) {
sharingLevelRaw, ok := agentproto.WorkspaceApp_SharingLevel_value[strings.ToUpper(string(dbApp.SharingLevel))]
if !ok {
return nil, xerrors.Errorf("unknown app sharing level: %q", dbApp.SharingLevel)
@@ -219,6 +219,12 @@ func dbAppToProto(dbApp database.WorkspaceApp, agent database.WorkspaceAgent, ow
return nil, xerrors.Errorf("unknown app health: %q", dbApp.SharingLevel)
}
// SubdomainName should be empty if AppHostname is not configured
subdomainName := ""
if appHostname != "" {
subdomainName = db2sdk.AppSubdomain(dbApp, agent.Name, workspace.Name, ownerName)
}
return &agentproto.WorkspaceApp{
Id: dbApp.ID[:],
Url: dbApp.Url.String,
@@ -228,7 +234,7 @@ func dbAppToProto(dbApp database.WorkspaceApp, agent database.WorkspaceAgent, ow
Command: dbApp.Command.String,
Icon: dbApp.Icon,
Subdomain: dbApp.Subdomain,
SubdomainName: db2sdk.AppSubdomain(dbApp, agent.Name, workspace.Name, ownerName),
SubdomainName: subdomainName,
SharingLevel: agentproto.WorkspaceApp_SharingLevel(sharingLevelRaw),
Healthcheck: &agentproto.WorkspaceApp_Healthcheck{
Url: dbApp.HealthcheckUrl,
+120 -59
View File
@@ -191,65 +191,6 @@ func TestGetManifest(t *testing.T) {
// These are done manually to ensure the conversion logic matches what a
// human expects.
var (
protoApps = []*agentproto.WorkspaceApp{
{
Id: apps[0].ID[:],
Url: apps[0].Url.String,
External: apps[0].External,
Slug: apps[0].Slug,
DisplayName: apps[0].DisplayName,
Command: apps[0].Command.String,
Icon: apps[0].Icon,
Subdomain: apps[0].Subdomain,
SubdomainName: fmt.Sprintf("%s--%s--%s--%s", apps[0].Slug, agent.Name, workspace.Name, owner.Username),
SharingLevel: agentproto.WorkspaceApp_AUTHENTICATED,
Healthcheck: &agentproto.WorkspaceApp_Healthcheck{
Url: apps[0].HealthcheckUrl,
Interval: durationpb.New(time.Duration(apps[0].HealthcheckInterval) * time.Second),
Threshold: apps[0].HealthcheckThreshold,
},
Health: agentproto.WorkspaceApp_HEALTHY,
Hidden: false,
},
{
Id: apps[1].ID[:],
Url: apps[1].Url.String,
External: apps[1].External,
Slug: apps[1].Slug,
DisplayName: apps[1].DisplayName,
Command: apps[1].Command.String,
Icon: apps[1].Icon,
Subdomain: false,
SubdomainName: "",
SharingLevel: agentproto.WorkspaceApp_PUBLIC,
Healthcheck: &agentproto.WorkspaceApp_Healthcheck{
Url: "",
Interval: durationpb.New(0),
Threshold: 0,
},
Health: agentproto.WorkspaceApp_DISABLED,
Hidden: false,
},
{
Id: apps[2].ID[:],
Url: apps[2].Url.String,
External: apps[2].External,
Slug: apps[2].Slug,
DisplayName: apps[2].DisplayName,
Command: apps[2].Command.String,
Icon: apps[2].Icon,
Subdomain: false,
SubdomainName: "",
SharingLevel: agentproto.WorkspaceApp_OWNER,
Healthcheck: &agentproto.WorkspaceApp_Healthcheck{
Url: apps[2].HealthcheckUrl,
Interval: durationpb.New(time.Duration(apps[2].HealthcheckInterval) * time.Second),
Threshold: apps[2].HealthcheckThreshold,
},
Health: agentproto.WorkspaceApp_UNHEALTHY,
Hidden: true,
},
}
protoScripts = []*agentproto.WorkspaceAgentScript{
{
Id: scripts[0].ID[:],
@@ -308,6 +249,66 @@ func TestGetManifest(t *testing.T) {
t.Run("OK", func(t *testing.T) {
t.Parallel()
protoApps := []*agentproto.WorkspaceApp{
{
Id: apps[0].ID[:],
Url: apps[0].Url.String,
External: apps[0].External,
Slug: apps[0].Slug,
DisplayName: apps[0].DisplayName,
Command: apps[0].Command.String,
Icon: apps[0].Icon,
Subdomain: apps[0].Subdomain,
SubdomainName: fmt.Sprintf("%s--%s--%s", apps[0].Slug, workspace.Name, owner.Username),
SharingLevel: agentproto.WorkspaceApp_AUTHENTICATED,
Healthcheck: &agentproto.WorkspaceApp_Healthcheck{
Url: apps[0].HealthcheckUrl,
Interval: durationpb.New(time.Duration(apps[0].HealthcheckInterval) * time.Second),
Threshold: apps[0].HealthcheckThreshold,
},
Health: agentproto.WorkspaceApp_HEALTHY,
Hidden: false,
},
{
Id: apps[1].ID[:],
Url: apps[1].Url.String,
External: apps[1].External,
Slug: apps[1].Slug,
DisplayName: apps[1].DisplayName,
Command: apps[1].Command.String,
Icon: apps[1].Icon,
Subdomain: false,
SubdomainName: "",
SharingLevel: agentproto.WorkspaceApp_PUBLIC,
Healthcheck: &agentproto.WorkspaceApp_Healthcheck{
Url: "",
Interval: durationpb.New(0),
Threshold: 0,
},
Health: agentproto.WorkspaceApp_DISABLED,
Hidden: false,
},
{
Id: apps[2].ID[:],
Url: apps[2].Url.String,
External: apps[2].External,
Slug: apps[2].Slug,
DisplayName: apps[2].DisplayName,
Command: apps[2].Command.String,
Icon: apps[2].Icon,
Subdomain: false,
SubdomainName: "",
SharingLevel: agentproto.WorkspaceApp_OWNER,
Healthcheck: &agentproto.WorkspaceApp_Healthcheck{
Url: apps[2].HealthcheckUrl,
Interval: durationpb.New(time.Duration(apps[2].HealthcheckInterval) * time.Second),
Threshold: apps[2].HealthcheckThreshold,
},
Health: agentproto.WorkspaceApp_UNHEALTHY,
Hidden: true,
},
}
mDB := dbmock.NewMockStore(gomock.NewController(t))
api := &agentapi.ManifestAPI{
@@ -438,6 +439,66 @@ func TestGetManifest(t *testing.T) {
t.Run("NoAppHostname", func(t *testing.T) {
t.Parallel()
protoApps := []*agentproto.WorkspaceApp{
{
Id: apps[0].ID[:],
Url: apps[0].Url.String,
External: apps[0].External,
Slug: apps[0].Slug,
DisplayName: apps[0].DisplayName,
Command: apps[0].Command.String,
Icon: apps[0].Icon,
Subdomain: apps[0].Subdomain,
SubdomainName: "", // Empty because AppHostname is empty
SharingLevel: agentproto.WorkspaceApp_AUTHENTICATED,
Healthcheck: &agentproto.WorkspaceApp_Healthcheck{
Url: apps[0].HealthcheckUrl,
Interval: durationpb.New(time.Duration(apps[0].HealthcheckInterval) * time.Second),
Threshold: apps[0].HealthcheckThreshold,
},
Health: agentproto.WorkspaceApp_HEALTHY,
Hidden: false,
},
{
Id: apps[1].ID[:],
Url: apps[1].Url.String,
External: apps[1].External,
Slug: apps[1].Slug,
DisplayName: apps[1].DisplayName,
Command: apps[1].Command.String,
Icon: apps[1].Icon,
Subdomain: false,
SubdomainName: "",
SharingLevel: agentproto.WorkspaceApp_PUBLIC,
Healthcheck: &agentproto.WorkspaceApp_Healthcheck{
Url: "",
Interval: durationpb.New(0),
Threshold: 0,
},
Health: agentproto.WorkspaceApp_DISABLED,
Hidden: false,
},
{
Id: apps[2].ID[:],
Url: apps[2].Url.String,
External: apps[2].External,
Slug: apps[2].Slug,
DisplayName: apps[2].DisplayName,
Command: apps[2].Command.String,
Icon: apps[2].Icon,
Subdomain: false,
SubdomainName: "",
SharingLevel: agentproto.WorkspaceApp_OWNER,
Healthcheck: &agentproto.WorkspaceApp_Healthcheck{
Url: apps[2].HealthcheckUrl,
Interval: durationpb.New(time.Duration(apps[2].HealthcheckInterval) * time.Second),
Threshold: apps[2].HealthcheckThreshold,
},
Health: agentproto.WorkspaceApp_UNHEALTHY,
Hidden: true,
},
}
mDB := dbmock.NewMockStore(gomock.NewController(t))
api := &agentapi.ManifestAPI{