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

View File

@@ -11,8 +11,10 @@ import (
"fmt" "fmt"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/uptrace/bun" "github.com/uptrace/bun"
"strings"
"time" "time"
) )
@@ -252,6 +254,14 @@ func (mgr *templateManager) buildTemplate(ctx context.Context, template *templat
BuildArgs: opts.buildArgs, BuildArgs: opts.buildArgs,
}) })
if err != nil { 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 return nil, err
} }

17
main.go
View File

@@ -10,6 +10,7 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"tesseract/internal/apierror"
"tesseract/internal/migration" "tesseract/internal/migration"
"tesseract/internal/service" "tesseract/internal/service"
"tesseract/internal/template" "tesseract/internal/template"
@@ -86,10 +87,20 @@ func main() {
c.Logger().Error(err) c.Logger().Error(err)
_ = c.NoContent(http.StatusInternalServerError) _ = c.NoContent(http.StatusInternalServerError)
} }
} else { return
c.Logger().Error(err)
_ = c.NoContent(http.StatusInternalServerError)
} }
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")) apiServer.Logger.Fatal(apiServer.Start(":8080"))

View File

@@ -1,5 +1,12 @@
import { promiseOrThrow } from "./lib/errors"; import { promiseOrThrow } from "./lib/errors";
interface ApiErrorResponse {
code: string;
error: string;
}
const API_ERROR_BAD_TEMPLATE = "BAD_TEMPLATE";
enum ApiError { enum ApiError {
NotFound = "NOT_FOUND", NotFound = "NOT_FOUND",
BadRequest = "BAD_REQUEST", BadRequest = "BAD_REQUEST",
@@ -16,10 +23,9 @@ async function fetchApi(
() => ApiError.Network, () => ApiError.Network,
); );
if (res.status !== 200) { if (res.status !== 200) {
console.log(res.status);
switch (res.status) { switch (res.status) {
case 401: case 400:
throw ApiError.BadRequest; throw await res.json();
case 404: case 404:
throw ApiError.NotFound; throw ApiError.NotFound;
default: default:
@@ -29,4 +35,15 @@ async function fetchApi(
return res; 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", Accept: "text/event-stream",
}, },
}); });
if (res.status !== 200) {
const errBody = await res.json();
throw errBody;
}
const stream = res.body?.pipeThrough(new TextDecoderStream()).getReader(); const stream = res.body?.pipeThrough(new TextDecoderStream()).getReader();
if (stream) { if (stream) {
while (true) { while (true) {

View File

@@ -2,6 +2,7 @@ import { createStore, useStore } from "zustand";
import type { Template } from "./types"; import type { Template } from "./types";
import { createContext, useContext } from "react"; import { createContext, useContext } from "react";
import { buildTemplate } from "./api"; import { buildTemplate } from "./api";
import { isApiErrorResponse, type ApiErrorResponse } from "@/api";
interface TemplateEditorState { interface TemplateEditorState {
template: Template; template: Template;
@@ -9,6 +10,7 @@ interface TemplateEditorState {
isBuildInProgress: boolean; isBuildInProgress: boolean;
isBuildOutputVisible: boolean; isBuildOutputVisible: boolean;
buildOutput: string; buildOutput: string;
buildError: ApiErrorResponse | null;
startBuild: ({ startBuild: ({
imageTag, imageTag,
@@ -34,6 +36,7 @@ function createTemplateEditorStore({
isBuildInProgress: false, isBuildInProgress: false,
isBuildOutputVisible: false, isBuildOutputVisible: false,
buildOutput: "", buildOutput: "",
buildError: null,
startBuild: async ({ imageTag, buildArgs }) => { startBuild: async ({ imageTag, buildArgs }) => {
const state = get(); const state = get();
@@ -42,6 +45,7 @@ function createTemplateEditorStore({
isBuildInProgress: true, isBuildInProgress: true,
isBuildOutputVisible: true, isBuildOutputVisible: true,
buildOutput: "", buildOutput: "",
buildError: null,
}); });
try { try {
@@ -51,8 +55,12 @@ function createTemplateEditorStore({
templateName: state.template.name, templateName: state.template.name,
onBuildOutput: state.addBuildOutputChunk, onBuildOutput: state.addBuildOutputChunk,
}); });
} catch { } catch (error) {
// TODO: handle build error console.error(error);
if (isApiErrorResponse(error)) {
console.log("askdjskdjk");
set({ buildError: error });
}
} finally { } finally {
set({ isBuildInProgress: false }); 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 { CodeMirrorEditor } from "@/components/codemirror-editor";
import { Button } from "@/components/ui/button.tsx"; import { Button } from "@/components/ui/button.tsx";
import { Dialog, DialogTrigger } from "@/components/ui/dialog"; import { Dialog, DialogTrigger } from "@/components/ui/dialog";
@@ -35,7 +35,7 @@ import {
} from "./template-editor-store"; } from "./template-editor-store";
import type { Template } from "./types"; import type { Template } from "./types";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { ToastAction } from "@/components/ui/toast"; import { Toaster } from "@/components/ui/toaster";
function TemplateEditor() { function TemplateEditor() {
const { templateName, _splat } = templateEditorRoute.useParams(); const { templateName, _splat } = templateEditorRoute.useParams();
@@ -122,6 +122,8 @@ function _TemplateEditor({
<TemplateBuildOutputPanel /> <TemplateBuildOutputPanel />
</main> </main>
</div> </div>
<Toaster />
<BuildErrorToast />
</SidebarProvider> </SidebarProvider>
</TemplateEditorStoreContext.Provider> </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 }; export { TemplateEditor };