Skip to content

Commit 039c96e

Browse files
authored
Merge pull request #76 from docker/feat/backend
feat: add backend service to extension
1 parent ef2958d commit 039c96e

File tree

13 files changed

+917
-10
lines changed

13 files changed

+917
-10
lines changed

src/extension/.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
node_modules
22
ui/build
33
*.vsix
4-
binary/build
4+
binary/build
5+
data/

src/extension/Dockerfile

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
1-
FROM --platform=$BUILDPLATFORM node:21.6-alpine3.18 AS client-builder
1+
# syntax=docker/dockerfile:1.4
2+
FROM golang:1.24-alpine AS builder
3+
ENV CGO_ENABLED=0
4+
WORKDIR /backend
5+
COPY backend/go.* .
6+
RUN --mount=type=cache,target=/go/pkg/mod \
7+
--mount=type=cache,target=/root/.cache/go-build \
8+
go mod download
9+
COPY backend/. .
10+
RUN --mount=type=cache,target=/go/pkg/mod \
11+
--mount=type=cache,target=/root/.cache/go-build \
12+
go build -trimpath -ldflags="-s -w" -o bin/service
13+
14+
FROM --platform=$BUILDPLATFORM node:23-alpine3.20 AS client-builder
215
WORKDIR /ui
3-
# cache packages in layer
4-
COPY ui/package.json /ui/package.json
5-
COPY ui/package-lock.json /ui/package-lock.json
16+
COPY ui/package.json ui/package-lock.json ./
617
RUN --mount=type=cache,target=/usr/src/app/.npm \
718
npm set cache /usr/src/app/.npm && \
819
npm ci
9-
# install
10-
COPY ui /ui
20+
COPY ui/. .
1121
RUN npm run build
1222

13-
FROM scratch
23+
FROM alpine:3.20
1424
ARG TARGETARCH
1525
LABEL org.opencontainers.image.title="AI Tool Catalog" \
1626
org.opencontainers.image.description="AI Tool Catalog" \
@@ -24,12 +34,14 @@ LABEL org.opencontainers.image.title="AI Tool Catalog" \
2434
com.docker.extension.categories="utility-tools" \
2535
com.docker.extension.changelog="Added MCP catalog"
2636

37+
COPY --from=builder /backend/bin/service /
2738
COPY docker-compose.yaml .
2839
COPY metadata.json .
2940
COPY docker.svg /docker.svg
3041
COPY host-binary/dist/windows-${TARGETARCH}/host-binary.exe /windows/host-binary.exe
3142
COPY host-binary/dist/darwin-${TARGETARCH}/host-binary /darwin/host-binary
3243
COPY host-binary/dist/linux-${TARGETARCH}/host-binary /linux/host-binary
3344
COPY --from=client-builder /ui/build ui
45+
COPY data /data
3446

35-
CMD sleep 600
47+
CMD ["/service", "-socket", "/run/guest-services/backend.sock"]

src/extension/Makefile

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
IMAGE?=docker/labs-ai-tools-for-devs
1+
# IMAGE?=docker/labs-ai-tools-for-devs
2+
IMAGE?=docker/ai-tools
23
TAG?=0.2.29
34

45
BUILDER=buildx-multi-arch
@@ -33,3 +34,12 @@ help: ## Show this help
3334
@grep -E '^[0-9a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "$(INFO_COLOR)%-30s$(NO_COLOR) %s\n", $$1, $$2}'
3435

3536
.PHONY: help
37+
38+
debug: # Show Chrome-dev tools
39+
docker extension dev debug $(IMAGE):$(TAG)
40+
debug-reset: # Reset Chrome-dev tools
41+
docker extension dev reset $(IMAGE):$(TAG)
42+
debug-ui: # Set UI hot reload
43+
docker extension dev ui-source $(IMAGE):$(TAG) http://localhost:3000
44+
ui-run:
45+
cd ui && npm start

src/extension/backend/go.mod

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
module github.com/docker/dait
2+
3+
go 1.24
4+
5+
require (
6+
github.com/labstack/echo/v4 v4.13.3
7+
github.com/sirupsen/logrus v1.9.3
8+
gopkg.in/yaml.v3 v3.0.1
9+
)
10+
11+
require (
12+
github.com/labstack/gommon v0.4.2 // indirect
13+
github.com/mattn/go-colorable v0.1.14 // indirect
14+
github.com/mattn/go-isatty v0.0.20 // indirect
15+
github.com/valyala/bytebufferpool v1.0.0 // indirect
16+
github.com/valyala/fasttemplate v1.2.2 // indirect
17+
golang.org/x/crypto v0.36.0 // indirect
18+
golang.org/x/net v0.37.0 // indirect
19+
golang.org/x/sys v0.31.0 // indirect
20+
golang.org/x/text v0.23.0 // indirect
21+
golang.org/x/time v0.11.0 // indirect
22+
)

src/extension/backend/go.sum

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4+
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
5+
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
6+
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
7+
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
8+
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
9+
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
10+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
11+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
12+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
13+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
14+
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
15+
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
16+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
17+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
18+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
19+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
20+
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
21+
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
22+
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
23+
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
24+
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
25+
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
26+
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
27+
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
28+
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
29+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
30+
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
31+
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
32+
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
33+
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
34+
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
35+
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
36+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
37+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
38+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
39+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
40+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
package api
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"net"
7+
"net/http"
8+
"os"
9+
10+
"github.com/docker/dait/internal/models"
11+
"github.com/labstack/echo/v4"
12+
"github.com/labstack/echo/v4/middleware"
13+
"github.com/sirupsen/logrus"
14+
)
15+
16+
var (
17+
dataPath = "/data"
18+
logger = logrus.New()
19+
)
20+
21+
func Run(socketPath string) error {
22+
23+
logger.SetOutput(os.Stdout)
24+
25+
logMiddleware := middleware.LoggerWithConfig(middleware.LoggerConfig{
26+
Skipper: middleware.DefaultSkipper,
27+
Format: `{"time":"${time_rfc3339_nano}","id":"${id}",` +
28+
`"method":"${method}","uri":"${uri}",` +
29+
`"status":${status},"error":"${error}"` +
30+
`}` + "\n",
31+
CustomTimeFormat: "2006-01-02 15:04:05.00000",
32+
Output: logger.Writer(),
33+
})
34+
35+
logger.Infof("Starting listening on %s\n", socketPath)
36+
router := echo.New()
37+
router.HideBanner = true
38+
router.Use(logMiddleware)
39+
startURL := ""
40+
41+
ln, err := listen(socketPath)
42+
if err != nil {
43+
return err
44+
}
45+
router.Listener = ln
46+
47+
router.GET("/catalog", catalog)
48+
router.GET("/newcatalog", newCatalog)
49+
router.GET("/config", config)
50+
router.POST("/events", events)
51+
router.POST("/refresh", refresh)
52+
router.GET("/assets/*", func(ctx echo.Context) error {
53+
return asset(ctx, ctx.Param("*"))
54+
})
55+
router.GET("/clients", clients)
56+
57+
routes := router.Routes()
58+
for _, route := range routes {
59+
logger.Infof("Registered route: %s %s", route.Method, route.Path)
60+
}
61+
62+
return router.Start(startURL)
63+
}
64+
65+
func listen(path string) (net.Listener, error) {
66+
return net.Listen("unix", path)
67+
}
68+
69+
func asset(ctx echo.Context, path string) error {
70+
return ctx.File(dataPath + "/assets/" + path)
71+
}
72+
73+
func newCatalog(ctx echo.Context) error {
74+
_, err := os.Stat(dataPath + "/catalog.json")
75+
if err != nil {
76+
return ctx.JSON(http.StatusNotFound, map[string]string{"error": "catalog.json does not exist"})
77+
}
78+
return ctx.File(dataPath + "/catalog.json")
79+
}
80+
81+
func catalog(ctx echo.Context) error {
82+
_, err := os.Stat(dataPath + "/catalog.yaml")
83+
if err != nil {
84+
return ctx.JSON(http.StatusNotFound, map[string]string{"error": "catalog.yaml does not exist"})
85+
}
86+
return ctx.File(dataPath + "/catalog.yaml")
87+
}
88+
89+
func config(ctx echo.Context) error {
90+
_, err := os.Stat(dataPath + "/config.json")
91+
if err != nil {
92+
return ctx.JSON(http.StatusNotFound, map[string]string{"error": "config.json does not exist"})
93+
}
94+
return ctx.File(dataPath + "/config.json")
95+
}
96+
97+
func clients(ctx echo.Context) error {
98+
_, err := os.Stat(dataPath + "/clients.json")
99+
if err != nil {
100+
return ctx.JSON(http.StatusNotFound, map[string]string{"error": "clients.json does not exist"})
101+
}
102+
return ctx.File(dataPath + "/clients.json")
103+
}
104+
105+
func events(ctx echo.Context) error {
106+
// Send events to Docker API
107+
url := "https://api.docker.com/events/v1/track"
108+
apiKey := "3EEvlMngcn3meCbpuYoyC4k8TSF0dYcB5XIVix lt"
109+
110+
var records []models.Record
111+
if err := ctx.Bind(&records); err != nil {
112+
return ctx.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
113+
}
114+
115+
client := &http.Client{}
116+
jsonData, err := json.Marshal(records)
117+
if err != nil {
118+
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
119+
}
120+
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
121+
if err != nil {
122+
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
123+
}
124+
125+
req.Header.Set("Content-Type", "application/json")
126+
req.Header.Set("x-api-key", apiKey)
127+
req.Header.Set("x-test-key", "test")
128+
129+
resp, err := client.Do(req)
130+
if err != nil {
131+
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
132+
}
133+
defer resp.Body.Close()
134+
135+
return ctx.JSON(resp.StatusCode, map[string]string{"status": "success"})
136+
}
137+
138+
func init() {
139+
// Skip initialization during tests
140+
if os.Getenv("GO_TEST") == "1" {
141+
return
142+
}
143+
if os.Getenv("DATA_PATH") != "" {
144+
dataPath = os.Getenv("DATA_PATH")
145+
}
146+
catalogURL := os.Getenv("CATALOG_URL")
147+
if catalogURL == "" {
148+
catalogURL = "https://raw.githubusercontent.com/docker/labs-ai-tools-for-devs/refs/heads/main/prompts/catalog.yaml"
149+
}
150+
151+
err := initVolume()
152+
if err != nil {
153+
logger.Fatal(err)
154+
}
155+
// check if catalog.json exists
156+
_, err = os.Stat(dataPath + "/catalog.json")
157+
if err != nil {
158+
logger.Info("catalog.json does not exist, creating it")
159+
// load catalog.json
160+
err := models.RefreshCatalog(catalogURL, dataPath)
161+
if err != nil {
162+
logger.Fatal(err)
163+
}
164+
// refresh catalog.json
165+
tiles, err := models.ParseCatalog(dataPath)
166+
if err != nil {
167+
logger.Fatal(err)
168+
}
169+
// write catalog.json
170+
catalog, err := json.Marshal(tiles)
171+
if err != nil {
172+
logger.Fatal(err)
173+
}
174+
err = os.WriteFile(dataPath+"/catalog.json", catalog, 0644)
175+
if err != nil {
176+
logger.Fatal(err)
177+
}
178+
}
179+
180+
}
181+
182+
func refresh(ctx echo.Context) error {
183+
184+
logger.Info("Refreshing catalog")
185+
catalogURL := os.Getenv("CATALOG_URL")
186+
if catalogURL == "" {
187+
catalogURL = "https://raw.githubusercontent.com/docker/labs-ai-tools-for-devs/refs/heads/main/prompts/catalog.yaml"
188+
}
189+
190+
err := models.RefreshCatalog(catalogURL, dataPath)
191+
if err != nil {
192+
logger.Error(err)
193+
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
194+
}
195+
// refresh catalog.json
196+
tiles, err := models.ParseCatalog(dataPath)
197+
if err != nil {
198+
logger.Error(err)
199+
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
200+
}
201+
// write catalog.json
202+
catalog, err := json.Marshal(tiles)
203+
if err != nil {
204+
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
205+
}
206+
err = os.WriteFile(dataPath+"/catalog.json", catalog, 0644)
207+
if err != nil {
208+
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
209+
}
210+
return ctx.JSON(http.StatusOK, map[string]string{"status": "success"})
211+
}
212+
213+
type HTTPMessageBody struct {
214+
Message string
215+
}
216+
217+
func initVolume() error {
218+
// check if /data/assets exists
219+
_, err := os.Stat(dataPath + "/assets")
220+
if err != nil {
221+
logger.Info("Creating /data/assets")
222+
err := os.MkdirAll(dataPath+"/assets", 0755)
223+
if err != nil {
224+
return err
225+
}
226+
}
227+
return nil
228+
}

0 commit comments

Comments
 (0)