From 4c41d4ce07a05b68511c80e9347fa63805f4267e Mon Sep 17 00:00:00 2001 From: Kenneth Date: Wed, 4 Dec 2024 00:10:25 +0000 Subject: [PATCH] feat: handle port forward subdomain conflict --- internal/apierror/api_error.go | 2 +- internal/reverseproxy/errors.go | 5 +++ internal/reverseproxy/proxy.go | 7 +++- internal/workspace/errors.go | 10 +++++ internal/workspace/http_handlers.go | 4 ++ internal/workspace/workspace_manager.go | 18 ++++++++- web/src/workspaces/api.ts | 4 +- .../workspaces/workspace-port-info-tab.tsx | 38 ++++++++++++++++++- 8 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 internal/reverseproxy/errors.go diff --git a/internal/apierror/api_error.go b/internal/apierror/api_error.go index 003e016..c5c623a 100644 --- a/internal/apierror/api_error.go +++ b/internal/apierror/api_error.go @@ -5,7 +5,7 @@ import "fmt" type APIError struct { StatusCode int `json:"-"` Code string `json:"code"` - Message string `json:"error"` + Message string `json:"error,omitempty"` } func New(status int, code, message string) *APIError { diff --git a/internal/reverseproxy/errors.go b/internal/reverseproxy/errors.go new file mode 100644 index 0000000..27d2922 --- /dev/null +++ b/internal/reverseproxy/errors.go @@ -0,0 +1,5 @@ +package reverseproxy + +import "errors" + +var ErrPortMappingConflict = errors.New("port mapping conflict") diff --git a/internal/reverseproxy/proxy.go b/internal/reverseproxy/proxy.go index ba49c6f..080a508 100644 --- a/internal/reverseproxy/proxy.go +++ b/internal/reverseproxy/proxy.go @@ -43,9 +43,14 @@ func (p *ReverseProxy) Middleware() echo.MiddlewareFunc { } } -func (p *ReverseProxy) AddEntry(subdomain string, url *url.URL) { +func (p *ReverseProxy) AddEntry(subdomain string, url *url.URL) error { + _, ok := p.httpProxies[subdomain] + if ok { + return ErrPortMappingConflict + } proxy := httputil.NewSingleHostReverseProxy(url) p.httpProxies[subdomain] = proxy + return nil } func (p *ReverseProxy) RemoveEntry(subdomain string) { diff --git a/internal/workspace/errors.go b/internal/workspace/errors.go index 506cf81..93d9645 100644 --- a/internal/workspace/errors.go +++ b/internal/workspace/errors.go @@ -1,9 +1,19 @@ package workspace +import "strings" + type errWorkspaceExists struct { message string } +type errPortMappingConflicts struct { + conflicts []string +} + func (err *errWorkspaceExists) Error() string { return err.message } + +func (err *errPortMappingConflicts) Error() string { + return "Subdomain(s) already in use: " + strings.Join(err.conflicts, ", ") +} diff --git a/internal/workspace/http_handlers.go b/internal/workspace/http_handlers.go index c86e3f0..447cdf2 100644 --- a/internal/workspace/http_handlers.go +++ b/internal/workspace/http_handlers.go @@ -126,6 +126,10 @@ func updateWorkspace(c echo.Context, workspace *workspace) error { if len(body.PortMappings) > 0 { if err = mgr.addPortMappings(ctx, workspace, body.PortMappings); err != nil { + var errPortMappingConflicts *errPortMappingConflicts + if errors.As(err, &errPortMappingConflicts) { + return apierror.New(http.StatusConflict, "PORT_MAPPINGS_EXIST", err.Error()) + } return err } } diff --git a/internal/workspace/workspace_manager.go b/internal/workspace/workspace_manager.go index 1850542..cc2ca82 100644 --- a/internal/workspace/workspace_manager.go +++ b/internal/workspace/workspace_manager.go @@ -347,9 +347,23 @@ func (mgr workspaceManager) addPortMappings(ctx context.Context, workspace *work return err } + var conflictErr errPortMappingConflicts + for i := range portMappings { - portMappings[i].WorkspaceID = workspace.ID - mgr.reverseProxy.AddEntry(portMappings[i].Subdomain, urls[i]) + err = mgr.reverseProxy.AddEntry(portMappings[i].Subdomain, urls[i]) + if err != nil { + if errors.Is(err, reverseproxy.ErrPortMappingConflict) { + conflictErr.conflicts = append(conflictErr.conflicts, portMappings[i].Subdomain) + } else { + return err + } + } else { + portMappings[i].WorkspaceID = workspace.ID + } + } + + if len(conflictErr.conflicts) > 0 { + return &conflictErr } _, err = tx.NewInsert().Model(&portMappings).Exec(ctx) diff --git a/web/src/workspaces/api.ts b/web/src/workspaces/api.ts index 4bb1ef5..60a5dff 100644 --- a/web/src/workspaces/api.ts +++ b/web/src/workspaces/api.ts @@ -165,7 +165,7 @@ function useDeleteWorkspace() { } function useAddWorkspacePort() { - const [status, setStatus] = useState({ type: "idle" }); + const [status, setStatus] = useState>({ type: "idle" }); const { mutate } = useSWRConfig(); const addWorkspacePort = useCallback( @@ -191,7 +191,7 @@ function useAddWorkspacePort() { ); setStatus({ type: "ok" }); } catch (error: unknown) { - setStatus({ type: "error", error }); + setStatus({ type: "error", error: error as ApiError }); } }, [mutate], diff --git a/web/src/workspaces/workspace-port-info-tab.tsx b/web/src/workspaces/workspace-port-info-tab.tsx index 3273ed1..36738ff 100644 --- a/web/src/workspaces/workspace-port-info-tab.tsx +++ b/web/src/workspaces/workspace-port-info-tab.tsx @@ -94,6 +94,7 @@ const NewPortMappingForm = object({ function NewPortMappingRow() { const { addWorkspacePort, status } = useAddWorkspacePort(); + const { toast } = useToast(); const workspace = useContext(WorkspaceTableRowContext); const isAddingPort = useStore((state) => state.isAddingPort); const setIsAddingPort = useStore((state) => state.setIsAddingPort); @@ -107,6 +108,42 @@ function NewPortMappingRow() { }, }); + useEffect(() => { + switch (status.type) { + case "error": + switch (status.error.type) { + case "CONFLICT": + toast({ + variant: "destructive", + title: "Subdomain already in use!", + description: "Please use another subdomain for this port.", + }); + break; + + case "NETWORK": + toast({ + variant: "destructive", + title: "Failed to forward port.", + description: "Network error.", + }); + break; + + default: + toast({ + variant: "destructive", + title: "Failed to forward port.", + description: "Unkown error.", + }); + break; + } + break; + + case "ok": + setIsAddingPort(false); + break; + } + }, [status]); + if (!isAddingPort) { return null; } @@ -118,7 +155,6 @@ function NewPortMappingRow() { port: values.port, }, ]); - setIsAddingPort(false); } return (