Skip to content
Draft
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
20 changes: 18 additions & 2 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,41 @@ Both backends expose the same core endpoints. The Go backend also includes a
- body: `{ "url": "https://github.com/user/repo.git" }`
- returns: array of commits `[{ hash, message, author, date }]`
- `POST /metadata`
- body: `{ "url": "...", "commit": "<sha>" }`
- returns: decoded `root.json` and `targets.json` metadata for the
specified commit
- `POST /metadata-single`
- body: `{ "url": "...", "commit": "<sha>", "file": "root.json|targets.json" }`
- returns: decoded metadata JSON from `metadata/<file>` at the specified
commit
- `POST /policy-query`
- body: `{ "url": "...", "commit": "<sha>", "branch": "main", "changedPath": "src/app.ts" }`
- returns: matched rule, required approvals, and authorized users for the
queried branch/path

- Local repository (by folder path)
- `POST /commits-local`
- body: `{ "path": "/absolute/path/to/local/repo" }`
- returns: array of commits from `HEAD`
- returns: array of commits from `refs/gittuf/policy`
- `POST /metadata-local`
- body: `{ "path": "...", "commit": "<sha>" }`
- returns: decoded `root.json` and `targets.json` metadata for the
specified commit
- `POST /metadata-local-single`
- body: `{ "path": "...", "commit": "<sha>", "file": "root.json|targets.json" }`
- returns: decoded metadata JSON from `metadata/<file>` at the specified
commit
- `POST /policy-query-local`
- body: `{ "path": "...", "commit": "<sha>", "branch": "main", "changedPath": "src/app.ts" }`
- returns: matched rule, required approvals, and authorized users for the
queried branch/path

- Health (Go backend only)
- `GET /health` → `{ "status": "Looks good!" }`

Notes:
- The backend fetches commits from the `refs/gittuf/policy` ref for
remote repositories.
both remote and local repositories.
- Metadata blobs are expected under `metadata/root.json` or
`metadata/targets.json` in the tree of the given commit.

Expand Down
132 changes: 128 additions & 4 deletions go-backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,38 @@ Fetches commits from the `refs/gittuf/policy` branch of a remote repository.

#### POST `/metadata`

Retrieves and decodes a metadata blob from a specific commit in a remote repository.
Retrieves and decodes both `root.json` and `targets.json` from a specific commit in a remote repository.

- **Request Body** (JSON):
```json
{
"url": "https://github.com/user/repo.git",
"commit": "abc123..."
}
```
- **Response** (200 OK):
```json
{
"root": {
// Decoded root metadata object
},
"targets": {
// Decoded targets metadata object
}
}
```
- **Error Response** (400/500):
```json
{
"error": "Error message",
"code": 400,
"details": "Detailed error information"
}
```

#### POST `/metadata-single`

Retrieves and decodes a single metadata blob from a specific commit in a remote repository.

- **Request Body** (JSON):
```json
Expand All @@ -118,13 +149,44 @@ Retrieves and decodes a metadata blob from a specific commit in a remote reposit
}
```

#### POST `/policy-query`

Evaluates a branch/path query against policy metadata at a specific remote policy commit.

- **Request Body** (JSON):
```json
{
"url": "https://github.com/user/repo.git",
"commit": "abc123...",
"branch": "main",
"changedPath": "src/app.ts"
}
```
- **Response** (200 OK):
```json
{
"matchedBranch": "main",
"matchedRule": "src/**",
"requiredApprovals": 2,
"authorizedUsers": ["alice", "bob"]
}
```
- **Error Response** (400/500):
```json
{
"error": "Error message",
"code": 400,
"details": "Detailed error information"
}
```

---

### Local Repository Endpoints

#### POST `/commits-local`

Lists commits from the HEAD of a local Git repository.
Lists commits from the `refs/gittuf/policy` ref of a local Git repository.

- **Request Body** (JSON):
```json
Expand Down Expand Up @@ -154,7 +216,38 @@ Lists commits from the HEAD of a local Git repository.

#### POST `/metadata-local`

Retrieves and decodes a metadata blob from a specific commit in a local repository.
Retrieves and decodes both `root.json` and `targets.json` from a specific commit in a local repository.

- **Request Body** (JSON):
```json
{
"path": "/path/to/local/repo",
"commit": "abc123..."
}
```
- **Response** (200 OK):
```json
{
"root": {
// Decoded root metadata object
},
"targets": {
// Decoded targets metadata object
}
}
```
- **Error Response** (400/500):
```json
{
"error": "Error message",
"code": 400,
"details": "Detailed error information"
}
```

#### POST `/metadata-local-single`

Retrieves and decodes a single metadata blob from a specific commit in a local repository.

- **Request Body** (JSON):
```json
Expand All @@ -179,6 +272,37 @@ Retrieves and decodes a metadata blob from a specific commit in a local reposito
}
```

#### POST `/policy-query-local`

Evaluates a branch/path query against policy metadata at a specific local policy commit.

- **Request Body** (JSON):
```json
{
"path": "/path/to/local/repo",
"commit": "abc123...",
"branch": "main",
"changedPath": "src/app.ts"
}
```
- **Response** (200 OK):
```json
{
"matchedBranch": "main",
"matchedRule": "src/**",
"requiredApprovals": 2,
"authorizedUsers": ["alice", "bob"]
}
```
- **Error Response** (400/500):
```json
{
"error": "Error message",
"code": 400,
"details": "Detailed error information"
}
```

## Project Structure

```
Expand All @@ -194,4 +318,4 @@ go-backend/
├── go.mod # Go module definition and dependencies
├── go.sum # Dependencies checksums
└── .env # Environment configuration
```
```
9 changes: 8 additions & 1 deletion go-backend/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,24 @@ func main() {
config.AllowHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization"}
router.Use(cors.New(config))

// health check endpoint
router.GET("/health", handlers.Health)

// Remote repository endpoints
router.POST("/commits", handlers.ListCommits)
router.POST("/metadata", handlers.GetMetadata)
router.POST("/metadata-single", handlers.GetMetadataSingle)
router.POST("/policy-query", handlers.QueryPolicyRemote)

// Local repository endpoints
router.POST("/commits-local", handlers.ListCommitsLocal)
router.POST("/metadata-local", handlers.GetMetadataLocal)
router.POST("/metadata-local-single", handlers.GetMetadataLocalSingle)
router.POST("/policy-query-local", handlers.QueryPolicyLocal)

port := os.Getenv("PORT")
if port == "" {
port = "5000"
port = "8080"
}

if err := router.Run(":" + port); err != nil {
Expand Down
17 changes: 17 additions & 0 deletions go-backend/internal/handlers/health.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright The gittuf Authors
// SPDX-License-Identifier: Apache-2.0

package handlers

import (
"net/http"

"github.com/gin-gonic/gin"
)

// simple health check endpoint for the backend server
func Health(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "Looks good!",
})
}
105 changes: 105 additions & 0 deletions go-backend/internal/handlers/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright The gittuf Authors
// SPDX-License-Identifier: Apache-2.0

package handlers

import (
"errors"
"fmt"
"net/http"
"strings"

"github.com/gittuf/visualizer/go-backend/internal/logger"
"github.com/gittuf/visualizer/go-backend/internal/models"
"github.com/gittuf/visualizer/go-backend/internal/services"
"github.com/gittuf/visualizer/go-backend/internal/validation"
"github.com/go-git/go-git/v5/plumbing/object"

"github.com/gin-gonic/gin"
)

func loadRemoteRepo(c *gin.Context, url, endpoint string) (string, func(), bool) {
repoPath, cleanup, err := services.CloneAndFetchRepo(url)
if err != nil {
logger.Sugar.Errorf("Exception in %s: %v", endpoint, err)
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
Error: "Failed to clone or fetch repository",
Code: http.StatusInternalServerError,
Details: err.Error(),
})
return "", nil, false
}

return repoPath, cleanup, true
}

func validateLocalRepoPath(c *gin.Context, path string) (string, bool) {
absPath, err := validation.GetAbsolutePath(path)
if err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Error: "Invalid path",
Code: http.StatusBadRequest,
Details: err.Error(),
})
return "", false
}

if !validation.PathExists(absPath) {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Error: fmt.Sprintf("Path does not exist: %s", absPath),
Code: http.StatusBadRequest,
Details: absPath,
})
return "", false
}

if !validation.IsValidGitRepo(absPath) {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Error: fmt.Sprintf("Not a valid Git repository: %s", absPath),
Code: http.StatusBadRequest,
Details: absPath,
})
return "", false
}

return absPath, true
}

func writeBindError(c *gin.Context, err error) {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Error: "Invalid request body",
Code: http.StatusBadRequest,
Details: err.Error(),
})
}

func requireFields(c *gin.Context, fields ...string) bool {
missing := make([]string, 0, len(fields)/2)
for i := 0; i+1 < len(fields); i += 2 {
if fields[i+1] == "" {
missing = append(missing, fmt.Sprintf("'%s'", fields[i]))
}
}
if len(missing) == 0 {
return true
}

c.JSON(http.StatusBadRequest, models.ErrorResponse{
Error: fmt.Sprintf("Missing required field(s): %s", strings.Join(missing, ", ")),
Code: http.StatusBadRequest,
})
return false
}

func writeMetadataError(c *gin.Context, action string, err error) {
status := http.StatusInternalServerError
if errors.Is(err, object.ErrFileNotFound) {
status = http.StatusNotFound
}

c.JSON(status, models.ErrorResponse{
Error: action,
Code: status,
Details: err.Error(),
})
}
Loading