diff --git a/coderd/database/db2sdk/db2sdk.go b/coderd/database/db2sdk/db2sdk.go index 48f6ff44af70f..62401f088c101 100644 --- a/coderd/database/db2sdk/db2sdk.go +++ b/coderd/database/db2sdk/db2sdk.go @@ -539,6 +539,23 @@ func AppSubdomain(dbApp database.WorkspaceApp, agentName, workspaceName, ownerNa }.String() } +// validateAppHostnameLength checks if any segment of the app hostname exceeds the DNS label limit of 63 characters. +// Returns true if the hostname is valid, false if any segment is too long. +func validateAppHostnameLength(subdomainName string) bool { + if subdomainName == "" { + return true + } + + // Split the hostname into segments by '--' (the format is app--agent--workspace--user) + segments := strings.Split(subdomainName, "--") + for _, segment := range segments { + if len(segment) > 63 { + return false + } + } + return true +} + func Apps(dbApps []database.WorkspaceApp, statuses []database.WorkspaceAppStatus, agent database.WorkspaceAgent, ownerName string, workspace database.Workspace) []codersdk.WorkspaceApp { sort.Slice(dbApps, func(i, j int) bool { if dbApps[i].DisplayOrder != dbApps[j].DisplayOrder { @@ -558,6 +575,15 @@ func Apps(dbApps []database.WorkspaceApp, statuses []database.WorkspaceAppStatus apps := make([]codersdk.WorkspaceApp, 0) for _, dbApp := range dbApps { statuses := statusesByAppID[dbApp.ID] + subdomainName := AppSubdomain(dbApp, agent.Name, workspace.Name, ownerName) + appHealth := codersdk.WorkspaceAppHealth(dbApp.Health) + + // Check if this is a subdomain app with hostname length issues + if dbApp.Subdomain && subdomainName != "" && !validateAppHostnameLength(subdomainName) { + // Override health to unhealthy if hostname exceeds DNS limits + appHealth = codersdk.WorkspaceAppHealthUnhealthy + } + apps = append(apps, codersdk.WorkspaceApp{ ID: dbApp.ID, URL: dbApp.Url.String, @@ -567,14 +593,14 @@ func Apps(dbApps []database.WorkspaceApp, statuses []database.WorkspaceAppStatus Command: dbApp.Command.String, Icon: dbApp.Icon, Subdomain: dbApp.Subdomain, - SubdomainName: AppSubdomain(dbApp, agent.Name, workspace.Name, ownerName), + SubdomainName: subdomainName, SharingLevel: codersdk.WorkspaceAppSharingLevel(dbApp.SharingLevel), Healthcheck: codersdk.Healthcheck{ URL: dbApp.HealthcheckUrl, Interval: dbApp.HealthcheckInterval, Threshold: dbApp.HealthcheckThreshold, }, - Health: codersdk.WorkspaceAppHealth(dbApp.Health), + Health: appHealth, Group: dbApp.DisplayGroup.String, Hidden: dbApp.Hidden, OpenIn: codersdk.WorkspaceAppOpenIn(dbApp.OpenIn), diff --git a/coderd/database/db2sdk/hostname_validation_test.go b/coderd/database/db2sdk/hostname_validation_test.go new file mode 100644 index 0000000000000..8c2552abd3071 --- /dev/null +++ b/coderd/database/db2sdk/hostname_validation_test.go @@ -0,0 +1,122 @@ +package db2sdk + +import ( + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/codersdk" +) + +func TestValidateAppHostnameLength(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + subdomainName string + expected bool + }{ + { + name: "empty hostname", + subdomainName: "", + expected: true, + }, + { + name: "valid short hostname", + subdomainName: "app--agent--workspace--user", + expected: true, + }, + { + name: "valid hostname with max length segment", + subdomainName: "a12345678901234567890123456789012345678901234567890123456789012--agent--workspace--user", // 63 chars in first segment + expected: true, + }, + { + name: "invalid hostname with long app name", + subdomainName: "toolongappnamethatexceedsthednslimitof63charactersforsureandshouldfail--agent--workspace--user", // 78 chars in first segment + expected: false, + }, + { + name: "invalid hostname with long agent name", + subdomainName: "app--toolongagentnamethatexceedsthednslimitof63charactersforsureandshouldfail--workspace--user", // 72 chars in agent segment + expected: false, + }, + { + name: "invalid hostname with long workspace name", + subdomainName: "app--agent--toolongworkspacenamethatexceedsthednslimitof63charactersforsureandshouldfail--user", // 77 chars in workspace segment + expected: false, + }, + { + name: "invalid hostname with long username", + subdomainName: "app--agent--workspace--toolongusernamethatexceedsthednslimitof63charactersforsureandshouldfail", // 72 chars in username segment + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := validateAppHostnameLength(tt.subdomainName) + require.Equal(t, tt.expected, result) + }) + } +} + +func TestAppsWithHostnameLengthValidation(t *testing.T) { + t.Parallel() + + agent := database.WorkspaceAgent{ + ID: uuid.New(), + Name: "agent", + } + workspace := database.Workspace{ + ID: uuid.New(), + Name: "workspace", + } + ownerName := "user" + + tests := []struct { + name string + appSlug string + subdomain bool + expectedHealth codersdk.WorkspaceAppHealth + }{ + { + name: "non-subdomain app should not be affected", + appSlug: "toolongappnamethatexceedsthednslimitof63charactersforsureandshouldfail", + subdomain: false, + expectedHealth: codersdk.WorkspaceAppHealthHealthy, + }, + { + name: "short subdomain app should remain healthy", + appSlug: "app", + subdomain: true, + expectedHealth: codersdk.WorkspaceAppHealthHealthy, + }, + { + name: "long subdomain app should become unhealthy", + appSlug: "toolongappnamethatexceedsthednslimitof63charactersforsureandshouldfail", + subdomain: true, + expectedHealth: codersdk.WorkspaceAppHealthUnhealthy, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dbApps := []database.WorkspaceApp{ + { + ID: uuid.New(), + Slug: tt.appSlug, + DisplayName: "Test App", + Subdomain: tt.subdomain, + Health: database.WorkspaceAppHealthHealthy, // Start as healthy + }, + } + + apps := Apps(dbApps, []database.WorkspaceAppStatus{}, agent, ownerName, workspace) + require.Len(t, apps, 1) + require.Equal(t, tt.expectedHealth, apps[0].Health) + }) + } +} diff --git a/site/src/modules/resources/AppLink/AppLink.stories.tsx b/site/src/modules/resources/AppLink/AppLink.stories.tsx index 891ddd3c2af7d..9b94cac8dd355 100644 --- a/site/src/modules/resources/AppLink/AppLink.stories.tsx +++ b/site/src/modules/resources/AppLink/AppLink.stories.tsx @@ -155,6 +155,19 @@ export const HealthUnhealthy: Story = { }, }; +export const HealthUnhealthyDueToHostnameLength: Story = { + args: { + workspace: MockWorkspace, + app: { + ...MockWorkspaceApp, + health: "unhealthy", + subdomain: true, + subdomain_name: "very-long-application-name-that-exceeds-dns-limits-and-causes-failures--agent--workspace--user", + }, + agent: MockWorkspaceAgent, + }, +}; + export const InternalApp: Story = { args: { workspace: MockWorkspace, diff --git a/site/src/modules/resources/AppLink/AppLink.tsx b/site/src/modules/resources/AppLink/AppLink.tsx index 637f0287a4088..a82568c8860fd 100644 --- a/site/src/modules/resources/AppLink/AppLink.tsx +++ b/site/src/modules/resources/AppLink/AppLink.tsx @@ -17,6 +17,17 @@ import { AgentButton } from "../AgentButton"; import { BaseIcon } from "./BaseIcon"; import { ShareIcon } from "./ShareIcon"; +// Check if an app's hostname has segments that exceed DNS label limits (63 characters) +const hasHostnameLengthIssue = (app: TypesGen.WorkspaceApp): boolean => { + if (!app.subdomain || !app.subdomain_name) { + return false; + } + + // Split by '--' to get hostname segments (format: app--agent--workspace--user) + const segments = app.subdomain_name.split("--"); + return segments.some(segment => segment.length > 63); +}; + export const DisplayAppNameMap: Record = { port_forwarding_helper: "Ports", ssh_helper: "SSH", @@ -68,7 +79,13 @@ export const AppLink: FC = ({ css={{ color: theme.palette.warning.light }} /> ); - primaryTooltip = "Unhealthy"; + + // Check if the unhealthy status is due to hostname length issues + if (hasHostnameLengthIssue(app)) { + primaryTooltip = "App name too long for DNS hostname. Please use a shorter app name, workspace name, or username."; + } else { + primaryTooltip = "Unhealthy"; + } } if (!host && app.subdomain) { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy