feat: handle template build error

This commit is contained in:
2024-12-02 13:45:49 +00:00
parent 8a8582e06e
commit dc97c2c498
9 changed files with 123 additions and 11 deletions

View File

@@ -0,0 +1,17 @@
package apierror
import "fmt"
type APIError struct {
StatusCode int `json:"-"`
Code string `json:"code"`
Message string `json:"error"`
}
func New(status int, code, message string) *APIError {
return &APIError{status, code, message}
}
func (err *APIError) Error() string {
return fmt.Sprintf("%s: %s", err.Code, err.Message)
}

View File

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

View File

@@ -7,6 +7,7 @@ import (
"github.com/labstack/echo/v4"
"io"
"net/http"
"tesseract/internal/apierror"
"tesseract/internal/service"
)
@@ -136,6 +137,10 @@ func buildTemplate(c echo.Context, body postTemplateRequestBody) error {
buildArgs: body.BuildArgs,
})
if err != nil {
var errBadTemplate *errBadTemplate
if errors.As(err, &errBadTemplate) {
return apierror.New(http.StatusBadRequest, "BAD_TEMPLATE", errBadTemplate.message)
}
return err
}

View File

@@ -11,8 +11,10 @@ import (
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
"github.com/google/uuid"
"github.com/uptrace/bun"
"strings"
"time"
)
@@ -252,6 +254,14 @@ func (mgr *templateManager) buildTemplate(ctx context.Context, template *templat
BuildArgs: opts.buildArgs,
})
if err != nil {
if errdefs.IsInvalidParameter(err) {
return nil, &errBadTemplate{
// 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?)"
// we don't want the "error response..." part because it is meaningless
message: strings.Replace(err.Error(), "Error response from daemon: ", "", 1),
}
}
return nil, err
}

17
main.go
View File

@@ -10,6 +10,7 @@ import (
"net/http"
"os"
"path/filepath"
"tesseract/internal/apierror"
"tesseract/internal/migration"
"tesseract/internal/service"
"tesseract/internal/template"
@@ -86,10 +87,20 @@ func main() {
c.Logger().Error(err)
_ = c.NoContent(http.StatusInternalServerError)
}
} else {
c.Logger().Error(err)
_ = c.NoContent(http.StatusInternalServerError)
return
}
var apiErr *apierror.APIError
if errors.As(err, &apiErr) {
if err = c.JSON(apiErr.StatusCode, apiErr); err != nil {
c.Logger().Error(err)
_ = c.NoContent(http.StatusInternalServerError)
}
return
}
c.Logger().Error(err)
_ = c.NoContent(http.StatusInternalServerError)
}
apiServer.Logger.Fatal(apiServer.Start(":8080"))

View File

@@ -1,5 +1,12 @@
import { promiseOrThrow } from "./lib/errors";
interface ApiErrorResponse {
code: string;
error: string;
}
const API_ERROR_BAD_TEMPLATE = "BAD_TEMPLATE";
enum ApiError {
NotFound = "NOT_FOUND",
BadRequest = "BAD_REQUEST",
@@ -16,10 +23,9 @@ async function fetchApi(
() => ApiError.Network,
);
if (res.status !== 200) {
console.log(res.status);
switch (res.status) {
case 401:
throw ApiError.BadRequest;
case 400:
throw await res.json();
case 404:
throw ApiError.NotFound;
default:
@@ -29,4 +35,15 @@ async function fetchApi(
return res;
}
export { ApiError, fetchApi };
function isApiErrorResponse(error: unknown): error is ApiErrorResponse {
return (
error !== null &&
error !== undefined &&
typeof error === "object" &&
"code" in error &&
"error" in error
);
}
export { API_ERROR_BAD_TEMPLATE, ApiError, fetchApi, isApiErrorResponse };
export type { ApiErrorResponse };

View File

@@ -144,6 +144,10 @@ async function buildTemplate({
Accept: "text/event-stream",
},
});
if (res.status !== 200) {
const errBody = await res.json();
throw errBody;
}
const stream = res.body?.pipeThrough(new TextDecoderStream()).getReader();
if (stream) {
while (true) {

View File

@@ -2,6 +2,7 @@ import { createStore, useStore } from "zustand";
import type { Template } from "./types";
import { createContext, useContext } from "react";
import { buildTemplate } from "./api";
import { isApiErrorResponse, type ApiErrorResponse } from "@/api";
interface TemplateEditorState {
template: Template;
@@ -9,6 +10,7 @@ interface TemplateEditorState {
isBuildInProgress: boolean;
isBuildOutputVisible: boolean;
buildOutput: string;
buildError: ApiErrorResponse | null;
startBuild: ({
imageTag,
@@ -34,6 +36,7 @@ function createTemplateEditorStore({
isBuildInProgress: false,
isBuildOutputVisible: false,
buildOutput: "",
buildError: null,
startBuild: async ({ imageTag, buildArgs }) => {
const state = get();
@@ -42,6 +45,7 @@ function createTemplateEditorStore({
isBuildInProgress: true,
isBuildOutputVisible: true,
buildOutput: "",
buildError: null,
});
try {
@@ -51,8 +55,12 @@ function createTemplateEditorStore({
templateName: state.template.name,
onBuildOutput: state.addBuildOutputChunk,
});
} catch {
// TODO: handle build error
} catch (error) {
console.error(error);
if (isApiErrorResponse(error)) {
console.log("askdjskdjk");
set({ buildError: error });
}
} finally {
set({ isBuildInProgress: false });
}

View File

@@ -1,4 +1,4 @@
import { ApiError } from "@/api";
import { API_ERROR_BAD_TEMPLATE, ApiError } from "@/api";
import { CodeMirrorEditor } from "@/components/codemirror-editor";
import { Button } from "@/components/ui/button.tsx";
import { Dialog, DialogTrigger } from "@/components/ui/dialog";
@@ -35,7 +35,7 @@ import {
} from "./template-editor-store";
import type { Template } from "./types";
import { useToast } from "@/hooks/use-toast";
import { ToastAction } from "@/components/ui/toast";
import { Toaster } from "@/components/ui/toaster";
function TemplateEditor() {
const { templateName, _splat } = templateEditorRoute.useParams();
@@ -122,6 +122,8 @@ function _TemplateEditor({
<TemplateBuildOutputPanel />
</main>
</div>
<Toaster />
<BuildErrorToast />
</SidebarProvider>
</TemplateEditorStoreContext.Provider>
);
@@ -332,4 +334,33 @@ function BuildOutput() {
);
}
function BuildErrorToast() {
const buildError = useTemplateEditorStore((state) => state.buildError);
const { toast } = useToast();
useEffect(() => {
if (!buildError) return;
switch (buildError.code) {
case API_ERROR_BAD_TEMPLATE:
toast({
variant: "destructive",
title: "Invalid template",
description: buildError.error,
});
break;
default:
toast({
variant: "destructive",
title: "Unexpected error",
description: buildError.error,
});
break;
}
}, [buildError, toast]);
return false;
}
export { TemplateEditor };