feat: handle workspace conflict error

This commit is contained in:
2024-12-02 19:12:26 +00:00
parent 4c34689ceb
commit 5fa55493b7
12 changed files with 74 additions and 19 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

View File

@@ -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)
}

View File

@@ -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

View File

@@ -0,0 +1,9 @@
package workspace
type errWorkspaceExists struct {
message string
}
func (err *errWorkspaceExists) Error() string {
return err.message
}

View File

@@ -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
} }

View File

@@ -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
} }

View File

@@ -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 };

View File

@@ -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";

View File

@@ -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({