feat: handle template build error
This commit is contained in:
17
internal/apierror/api_error.go
Normal file
17
internal/apierror/api_error.go
Normal 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)
|
||||||
|
}
|
9
internal/template/errors.go
Normal file
9
internal/template/errors.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package template
|
||||||
|
|
||||||
|
type errBadTemplate struct {
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *errBadTemplate) Error() string {
|
||||||
|
return err.message
|
||||||
|
}
|
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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
17
main.go
@@ -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"))
|
||||||
|
@@ -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 };
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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 });
|
||||||
}
|
}
|
||||||
|
@@ -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 };
|
||||||
|
Reference in New Issue
Block a user