feat: handle port forward subdomain conflict

This commit is contained in:
2024-12-04 00:10:25 +00:00
parent ee857cbbf9
commit 4c41d4ce07
8 changed files with 81 additions and 7 deletions

View File

@@ -5,7 +5,7 @@ import "fmt"
type APIError struct { type APIError struct {
StatusCode int `json:"-"` StatusCode int `json:"-"`
Code string `json:"code"` Code string `json:"code"`
Message string `json:"error"` Message string `json:"error,omitempty"`
} }
func New(status int, code, message string) *APIError { func New(status int, code, message string) *APIError {

View File

@@ -0,0 +1,5 @@
package reverseproxy
import "errors"
var ErrPortMappingConflict = errors.New("port mapping conflict")

View File

@@ -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) proxy := httputil.NewSingleHostReverseProxy(url)
p.httpProxies[subdomain] = proxy p.httpProxies[subdomain] = proxy
return nil
} }
func (p *ReverseProxy) RemoveEntry(subdomain string) { func (p *ReverseProxy) RemoveEntry(subdomain string) {

View File

@@ -1,9 +1,19 @@
package workspace package workspace
import "strings"
type errWorkspaceExists struct { type errWorkspaceExists struct {
message string message string
} }
type errPortMappingConflicts struct {
conflicts []string
}
func (err *errWorkspaceExists) Error() string { func (err *errWorkspaceExists) Error() string {
return err.message return err.message
} }
func (err *errPortMappingConflicts) Error() string {
return "Subdomain(s) already in use: " + strings.Join(err.conflicts, ", ")
}

View File

@@ -126,6 +126,10 @@ func updateWorkspace(c echo.Context, workspace *workspace) error {
if len(body.PortMappings) > 0 { if len(body.PortMappings) > 0 {
if err = mgr.addPortMappings(ctx, workspace, body.PortMappings); err != nil { 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 return err
} }
} }

View File

@@ -347,9 +347,23 @@ func (mgr workspaceManager) addPortMappings(ctx context.Context, workspace *work
return err return err
} }
var conflictErr errPortMappingConflicts
for i := range portMappings { for i := range portMappings {
portMappings[i].WorkspaceID = workspace.ID err = mgr.reverseProxy.AddEntry(portMappings[i].Subdomain, urls[i])
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) _, err = tx.NewInsert().Model(&portMappings).Exec(ctx)

View File

@@ -165,7 +165,7 @@ function useDeleteWorkspace() {
} }
function useAddWorkspacePort() { function useAddWorkspacePort() {
const [status, setStatus] = useState<QueryStatus>({ type: "idle" }); const [status, setStatus] = useState<QueryStatus<ApiError>>({ type: "idle" });
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
const addWorkspacePort = useCallback( const addWorkspacePort = useCallback(
@@ -191,7 +191,7 @@ function useAddWorkspacePort() {
); );
setStatus({ type: "ok" }); setStatus({ type: "ok" });
} catch (error: unknown) { } catch (error: unknown) {
setStatus({ type: "error", error }); setStatus({ type: "error", error: error as ApiError });
} }
}, },
[mutate], [mutate],

View File

@@ -94,6 +94,7 @@ const NewPortMappingForm = object({
function NewPortMappingRow() { function NewPortMappingRow() {
const { addWorkspacePort, status } = useAddWorkspacePort(); const { addWorkspacePort, status } = useAddWorkspacePort();
const { toast } = useToast();
const workspace = useContext(WorkspaceTableRowContext); const workspace = useContext(WorkspaceTableRowContext);
const isAddingPort = useStore((state) => state.isAddingPort); const isAddingPort = useStore((state) => state.isAddingPort);
const setIsAddingPort = useStore((state) => state.setIsAddingPort); 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) { if (!isAddingPort) {
return null; return null;
} }
@@ -118,7 +155,6 @@ function NewPortMappingRow() {
port: values.port, port: values.port,
}, },
]); ]);
setIsAddingPort(false);
} }
return ( return (