feat: handle port forward subdomain conflict
This commit is contained in:
@@ -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 {
|
||||||
|
5
internal/reverseproxy/errors.go
Normal file
5
internal/reverseproxy/errors.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
package reverseproxy
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var ErrPortMappingConflict = errors.New("port mapping conflict")
|
@@ -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) {
|
||||||
|
@@ -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, ", ")
|
||||||
|
}
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
|
@@ -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],
|
||||||
|
@@ -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 (
|
||||||
|
Reference in New Issue
Block a user