Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ config.yaml
# Editor/IDE
.idea/
.vscode/

# LLM friends
CLAUDE.md
AGENTS.md
102 changes: 101 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Bedrock API

![Coverage](https://img.shields.io/badge/coverage-37.6%25-red)
![Coverage](https://img.shields.io/badge/coverage-38.6%25-red)

**Bedrock API** is an HTTP service that coordinates **Bedrock tracing workloads** through REST APIs and internal event-driven communication.

Expand Down Expand Up @@ -212,6 +212,40 @@ Creates:

* new session and assigns a DockerD

Request body (application/json):

```json
{
"image": "<docker-image>",
"command": "<command-to-run>",
"ttl": "<duration>"
}
```

Fields:

* image (string) - Docker image to run (e.g. "nginx:stable").
* command (string) - space-separated command string that will be split on spaces and passed to the container as an argv list.
* ttl (integer) - session time-to-live in seconds; DockerD will stop the session after this duration if it hasn't finished.

Example: start a short-lived nginx session

```json
{
"image": "nginx:stable",
"command": "nginx -g 'daemon off;'",
"ttl": "24h"
}
```

Example curl (create session):

```sh
curl -X POST http://localhost:8080/api/sessions \
-H "Content-Type: application/json" \
-d '{"image":"nginx:stable","command":"nginx -g \'daemon off;\'","ttl":60}'
```

### Stop Session

```http
Expand All @@ -222,6 +256,31 @@ Creates:

* patch event for DockerD

Request body (application/json):

```json
{ "status": "<status>" }
```

Notes:

* Only the `status` field is read by the API for updates. Valid status values are: `pending`, `running`, `stopped`, `finished`, `failed`.
* The server enforces a state-machine when applying status changes; invalid transitions will return HTTP 400.

Example: stop a running session

```json
{ "status": "stopped" }
```

Example curl (stop session):

```sh
curl -X PUT http://localhost:8080/api/sessions/<session-id> \
-H "Content-Type: application/json" \
-d '{"status":"stopped"}'
```

### List Sessions

```http
Expand All @@ -230,6 +289,30 @@ GET /api/sessions

Returns all sessions from KV storage.

Request body: none

Response (application/json): array of Session objects. Each Session has the shape:

```json
{
"id": "<uuid>",
"dockerd_id": "<dockerd-id>",
"created_at": "<timestamp>",
"status": "<status>",
"spec": {
"image": "<docker-image>",
"command": "<command-string>",
"ttl": <seconds>
}
}
```

Example curl (list sessions):

```sh
curl -X GET http://localhost:8080/api/sessions
```

### Get Session Logs

```http
Expand All @@ -238,6 +321,14 @@ GET /api/sessions/:id/logs

Returns session tracing logs.

Note: endpoint is currently not implemented in the codebase (returns HTTP 501).

Example curl (get session logs):

```sh
curl -X GET http://localhost:8080/api/sessions/<session-id>/logs
```

### Store Session Logs

```http
Expand All @@ -246,6 +337,15 @@ POST /api/sessions/:id/logs

Uploads session tracing logs.

Intended usage: upload tracing artifacts for the given session. The current handlers are not implemented in the codebase (returns HTTP 501). When implemented, this endpoint is expected to accept file uploads (multipart/form-data) containing the logs/artifacts for the session.

Example curl (upload session logs):

```sh
curl -X POST http://localhost:8080/api/sessions/<session-id>/logs \
-F "file=@/path/to/log.tar.gz"
```

## Requirements

* Docker
Expand Down
85 changes: 82 additions & 3 deletions internal/ports/http/handlers.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package http

import (
"encoding/json"
"net/http"
"time"

"github.com/amirhnajafiz/bedrock-api/pkg/enums"
"github.com/amirhnajafiz/bedrock-api/pkg/models"

"github.com/google/uuid"
"github.com/labstack/echo/v5"
"go.uber.org/zap"
)
Expand All @@ -23,17 +27,92 @@ func (h HTTPServer) health(c *echo.Context) error {

// createSession creates a new session based on the request payload and returns the session ID.
func (h HTTPServer) createSession(c *echo.Context) error {
return c.String(http.StatusNotImplemented, "Not implemented")
var raw map[string]json.RawMessage
if err := json.NewDecoder(c.Request().Body).Decode(&raw); err != nil {
return c.String(http.StatusBadRequest, "invalid request body")
}

var spec models.Spec
if err := json.Unmarshal(raw["image"], &spec.Image); err != nil {
return c.String(http.StatusBadRequest, "invalid image")
}
if err := json.Unmarshal(raw["command"], &spec.Command); err != nil {
return c.String(http.StatusBadRequest, "invalid command")
}

var ttlStr string
if err := json.Unmarshal(raw["ttl"], &ttlStr); err != nil {
return c.String(http.StatusBadRequest, "invalid ttl")
}
ttl, err := time.ParseDuration(ttlStr)
if err != nil {
return c.String(http.StatusBadRequest, "invalid ttl format (e.g. 1s, 1m, 1h)")
}
spec.TTL = ttl

// assign DockerD to this session
dockerd, err := h.scheduler.Pick()
if err != nil {
return c.String(http.StatusServiceUnavailable, "no docker daemons available")
}

// create and save session to KV store
session := models.Session{
Id: uuid.New().String(),
DockerDId: dockerd,
CreatedAt: time.Now(),
Status: enums.SessionStatusPending,
Spec: spec,
}
h.sessionStore.SaveSession(&session)

return c.JSON(http.StatusCreated, session)
}

// updateSession updates an existing session with the specified ID based on the request payload.
func (h HTTPServer) updateSession(c *echo.Context) error {
return c.String(http.StatusNotImplemented, "Not implemented")
// read path params
id := c.Param("id")

// payload only contains the new desired state
var payload struct {
Status enums.SessionStatus `json:"status"`
}
if err := c.Bind(&payload); err != nil {
return c.String(http.StatusBadRequest, "invalid request body")
}

// fetch existing session by id (store will find the owning dockerd)
session, err := h.sessionStore.GetSessionById(id)
if err != nil {
h.Logr.Warn("failed to get session", zap.Error(err), zap.String("session id", id))
return c.String(http.StatusNotFound, "session not found")
}

// apply state machine transition
newState := h.stateMachine.Transition(session.Status, payload.Status)
if newState == session.Status {
// transition not allowed or no-op
return c.String(http.StatusBadRequest, "invalid state transition")
}

session.Status = newState

if err := h.sessionStore.SaveSession(session); err != nil {
h.Logr.Warn("failed to save session", zap.Error(err), zap.String("session id", id), zap.String("dockerd id", session.DockerDId))
return c.String(http.StatusInternalServerError, "failed to save session")
}

return c.JSON(http.StatusOK, session)
}

// getSessions retrieves a list of all sessions and returns them in the response.
func (h HTTPServer) getSessions(c *echo.Context) error {
return c.String(http.StatusNotImplemented, "Not implemented")
sessions, err := h.sessionStore.ListSessions()
if err != nil {
return c.String(http.StatusInternalServerError, "failed to list sessions")
}
return c.JSON(http.StatusFound, sessions)
}

// getSessionLogs retrieves the logs for a specific session based on the session ID provided in the request parameters.
Expand Down
3 changes: 3 additions & 0 deletions internal/ports/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/amirhnajafiz/bedrock-api/internal/components/sessions"
zmqclient "github.com/amirhnajafiz/bedrock-api/internal/components/zmq_client"
"github.com/amirhnajafiz/bedrock-api/internal/scheduler"
"github.com/amirhnajafiz/bedrock-api/internal/state_machine"
"github.com/amirhnajafiz/bedrock-api/internal/storage"

"github.com/labstack/echo/v5"
Expand All @@ -23,6 +24,7 @@ type HTTPServer struct {
scheduler scheduler.Scheduler
sessionStore sessions.SessionStore
zclient *zmqclient.ZMQClient
stateMachine *statemachine.StateMachine
}

// NewHTTPServer creates and returns a new instance of HTTPServer.
Expand All @@ -32,6 +34,7 @@ func (h HTTPServer) Build(address, socketAddress string) *HTTPServer {
h.scheduler = scheduler.NewRoundRobin()
h.sessionStore = sessions.NewSessionStore(storage.NewGoCache())
h.zclient = zmqclient.NewZMQClient(socketAddress)
h.stateMachine = statemachine.NewStateMachine()

return &h
}
Expand Down
Loading