feat: handle workspace conflict error
This commit is contained in:
BIN
docs/screenshots/build-dialog.png
Normal file
BIN
docs/screenshots/build-dialog.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 100 KiB |
BIN
docs/screenshots/build-output-panel.png
Normal file
BIN
docs/screenshots/build-output-panel.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 546 KiB |
BIN
docs/screenshots/template-editor.png
Normal file
BIN
docs/screenshots/template-editor.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 253 KiB |
BIN
docs/screenshots/workspace-dialog.png
Normal file
BIN
docs/screenshots/workspace-dialog.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 146 KiB |
@@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ContainerSSHHostPort returns the port on the host that is exposing the internal ssh port of the given container info
|
// ContainerSSHHostPort returns the port on the host that is exposing the internal ssh port of the given container info
|
||||||
@@ -33,3 +34,8 @@ func ContainerHostPort(ctx context.Context, container types.ContainerJSON, port
|
|||||||
}
|
}
|
||||||
return port
|
return port
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CleanErrorMessage removes unnecessary parts in a docker sdk error message, such as "Error response from daemon:"
|
||||||
|
func CleanErrorMessage(msg string) string {
|
||||||
|
return strings.Replace(msg, "Error response from daemon: ", "", 1)
|
||||||
|
}
|
||||||
|
@@ -14,7 +14,7 @@ import (
|
|||||||
"github.com/docker/docker/errdefs"
|
"github.com/docker/docker/errdefs"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
"strings"
|
"tesseract/internal/docker"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -259,7 +259,7 @@ func (mgr *templateManager) buildTemplate(ctx context.Context, template *templat
|
|||||||
// the docker sdk returns an error message that looks like:
|
// the docker sdk returns an error message that looks like:
|
||||||
// "Error response from daemon: dockerfile parse error on line 1: unknown instruction: FR (did you mean FROM?)"
|
// "Error response from daemon: dockerfile parse error on line 1: unknown instruction: FR (did you mean FROM?)"
|
||||||
// we don't want the "error response..." part because it is meaningless
|
// we don't want the "error response..." part because it is meaningless
|
||||||
message: strings.Replace(err.Error(), "Error response from daemon: ", "", 1),
|
message: docker.CleanErrorMessage(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
|
9
internal/workspace/errors.go
Normal file
9
internal/workspace/errors.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package workspace
|
||||||
|
|
||||||
|
type errWorkspaceExists struct {
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *errWorkspaceExists) Error() string {
|
||||||
|
return err.message
|
||||||
|
}
|
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"tesseract/internal/apierror"
|
||||||
)
|
)
|
||||||
|
|
||||||
type createWorkspaceRequestBody struct {
|
type createWorkspaceRequestBody struct {
|
||||||
@@ -86,6 +87,12 @@ func createWorkspace(c echo.Context, workspaceName string) error {
|
|||||||
if errors.Is(err, errImageNotFound) {
|
if errors.Is(err, errImageNotFound) {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("no image with id %v exists", body.ImageID))
|
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("no image with id %v exists", body.ImageID))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var errWorkspaceExists *errWorkspaceExists
|
||||||
|
if errors.As(err, &errWorkspaceExists) {
|
||||||
|
return apierror.New(http.StatusBadRequest, "WORKSPACE_EXISTS", errWorkspaceExists.message)
|
||||||
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/docker/docker/errdefs"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
@@ -174,6 +175,11 @@ func (mgr workspaceManager) createWorkspace(ctx context.Context, opts createWork
|
|||||||
|
|
||||||
res, err := mgr.dockerClient.ContainerCreate(ctx, containerConfig, hostConfig, nil, nil, opts.name)
|
res, err := mgr.dockerClient.ContainerCreate(ctx, containerConfig, hostConfig, nil, nil, opts.name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errdefs.IsConflict(err) {
|
||||||
|
return nil, &errWorkspaceExists{
|
||||||
|
message: docker.CleanErrorMessage(err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -6,6 +6,7 @@ interface ApiErrorResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const API_ERROR_BAD_TEMPLATE = "BAD_TEMPLATE";
|
const API_ERROR_BAD_TEMPLATE = "BAD_TEMPLATE";
|
||||||
|
const API_ERROR_WORKSPACE_EXISTS = "WORKSPACE_EXISTS";
|
||||||
|
|
||||||
enum ApiError {
|
enum ApiError {
|
||||||
NotFound = "NOT_FOUND",
|
NotFound = "NOT_FOUND",
|
||||||
@@ -45,5 +46,11 @@ function isApiErrorResponse(error: unknown): error is ApiErrorResponse {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { API_ERROR_BAD_TEMPLATE, ApiError, fetchApi, isApiErrorResponse };
|
export {
|
||||||
|
API_ERROR_BAD_TEMPLATE,
|
||||||
|
API_ERROR_WORKSPACE_EXISTS,
|
||||||
|
ApiError,
|
||||||
|
fetchApi,
|
||||||
|
isApiErrorResponse,
|
||||||
|
};
|
||||||
export type { ApiErrorResponse };
|
export type { ApiErrorResponse };
|
||||||
|
@@ -15,7 +15,7 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Link, useRouter } from "@tanstack/react-router";
|
import { Link, useRouter } from "@tanstack/react-router";
|
||||||
import { ArrowLeft, ChevronDown, ChevronUp, Loader2 } from "lucide-react";
|
import { ArrowLeft, ChevronDown, ChevronUp, Loader2 } from "lucide-react";
|
||||||
import { useEffect, useId, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useStore } from "zustand";
|
import { useStore } from "zustand";
|
||||||
import { useTemplate, useTemplateFile, useUpdateTemplateFile } from "./api";
|
import { useTemplate, useTemplateFile, useUpdateTemplateFile } from "./api";
|
||||||
import { templateEditorRoute } from "./routes";
|
import { templateEditorRoute } from "./routes";
|
||||||
|
@@ -30,6 +30,7 @@ import { nonempty, object, pattern, string, type Infer } from "superstruct";
|
|||||||
import { useCreateWorkspace, useWorkspaceRuntimes } from "./api";
|
import { useCreateWorkspace, useWorkspaceRuntimes } from "./api";
|
||||||
import type { TemplateImage } from "@/templates/types";
|
import type { TemplateImage } from "@/templates/types";
|
||||||
import type { WorkspaceRuntime } from "./types";
|
import type { WorkspaceRuntime } from "./types";
|
||||||
|
import { API_ERROR_WORKSPACE_EXISTS, isApiErrorResponse } from "@/api";
|
||||||
|
|
||||||
interface NewWorkspaceDialogProps {
|
interface NewWorkspaceDialogProps {
|
||||||
onCreateSuccess: () => void;
|
onCreateSuccess: () => void;
|
||||||
@@ -131,28 +132,47 @@ function NewWorkspaceForm({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
switch (status.type) {
|
switch (status.type) {
|
||||||
case "error":
|
case "error":
|
||||||
toast({
|
if (isApiErrorResponse(status.error)) {
|
||||||
variant: "destructive",
|
let toastTitle = "";
|
||||||
title: "Failed to create the workspace.",
|
switch (status.error.code) {
|
||||||
action: (
|
case API_ERROR_WORKSPACE_EXISTS:
|
||||||
<ToastAction
|
toastTitle = "Workspace already exists.";
|
||||||
onClick={() => {
|
break;
|
||||||
formRef.current?.requestSubmit();
|
default:
|
||||||
}}
|
toastTitle = "Failed to create the workspace.";
|
||||||
altText="Try again"
|
break;
|
||||||
>
|
}
|
||||||
Try again
|
toast({
|
||||||
</ToastAction>
|
variant: "destructive",
|
||||||
),
|
title: toastTitle,
|
||||||
});
|
description: status.error.error,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Failed to create the workspace.",
|
||||||
|
description: "Unknown error",
|
||||||
|
action: (
|
||||||
|
<ToastAction
|
||||||
|
onClick={() => {
|
||||||
|
formRef.current?.requestSubmit();
|
||||||
|
}}
|
||||||
|
altText="Try again"
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</ToastAction>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "ok":
|
case "ok":
|
||||||
onCreateSuccess();
|
onCreateSuccess();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, [status.type, toast, onCreateSuccess]);
|
}, [status, toast, onCreateSuccess]);
|
||||||
|
|
||||||
async function onSubmit(values: Infer<typeof NewWorkspaceFormSchema>) {
|
async function onSubmit(values: Infer<typeof NewWorkspaceFormSchema>) {
|
||||||
await createWorkspace({
|
await createWorkspace({
|
||||||
|
Reference in New Issue
Block a user