Skip to content

Fix Coder apps with long hostname DNS validation and add informative user tooltips #19251

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions coderd/database/db2sdk/db2sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand All @@ -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),
Expand Down
122 changes: 122 additions & 0 deletions coderd/database/db2sdk/hostname_validation_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
13 changes: 13 additions & 0 deletions site/src/modules/resources/AppLink/AppLink.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
19 changes: 18 additions & 1 deletion site/src/modules/resources/AppLink/AppLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TypesGen.DisplayApp, string> = {
port_forwarding_helper: "Ports",
ssh_helper: "SSH",
Expand Down Expand Up @@ -68,7 +79,13 @@ export const AppLink: FC<AppLinkProps> = ({
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) {
Expand Down
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