Skip to content

Commit 1f7b7a2

Browse files
authored
Merge pull request #141 from kazuhidelee/tonylee/adding_new_api_endpoints
add: new api endpoints for the visualizer + modify the existing endpo…
2 parents 73c46ae + 6ad8bee commit 1f7b7a2

14 files changed

Lines changed: 848 additions & 147 deletions

File tree

docs/development.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,41 @@ Both backends expose the same core endpoints. The Go backend also includes a
2626
- body: `{ "url": "https://github.com/user/repo.git" }`
2727
- returns: array of commits `[{ hash, message, author, date }]`
2828
- `POST /metadata`
29+
- body: `{ "url": "...", "commit": "<sha>" }`
30+
- returns: decoded `root.json` and `targets.json` metadata for the
31+
specified commit
32+
- `POST /metadata-single`
2933
- body: `{ "url": "...", "commit": "<sha>", "file": "root.json|targets.json" }`
3034
- returns: decoded metadata JSON from `metadata/<file>` at the specified
3135
commit
36+
- `POST /policy-query`
37+
- body: `{ "url": "...", "commit": "<sha>", "branch": "main", "changedPath": "src/app.ts" }`
38+
- returns: matched rule, required approvals, and authorized users for the
39+
queried branch/path
3240

3341
- Local repository (by folder path)
3442
- `POST /commits-local`
3543
- body: `{ "path": "/absolute/path/to/local/repo" }`
36-
- returns: array of commits from `HEAD`
44+
- returns: array of commits from `refs/gittuf/policy`
3745
- `POST /metadata-local`
46+
- body: `{ "path": "...", "commit": "<sha>" }`
47+
- returns: decoded `root.json` and `targets.json` metadata for the
48+
specified commit
49+
- `POST /metadata-local-single`
3850
- body: `{ "path": "...", "commit": "<sha>", "file": "root.json|targets.json" }`
3951
- returns: decoded metadata JSON from `metadata/<file>` at the specified
4052
commit
53+
- `POST /policy-query-local`
54+
- body: `{ "path": "...", "commit": "<sha>", "branch": "main", "changedPath": "src/app.ts" }`
55+
- returns: matched rule, required approvals, and authorized users for the
56+
queried branch/path
4157

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

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

go-backend/README.md

Lines changed: 128 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,38 @@ Fetches commits from the `refs/gittuf/policy` branch of a remote repository.
9393

9494
#### POST `/metadata`
9595

96-
Retrieves and decodes a metadata blob from a specific commit in a remote repository.
96+
Retrieves and decodes both `root.json` and `targets.json` from a specific commit in a remote repository.
97+
98+
- **Request Body** (JSON):
99+
```json
100+
{
101+
"url": "https://github.com/user/repo.git",
102+
"commit": "abc123..."
103+
}
104+
```
105+
- **Response** (200 OK):
106+
```json
107+
{
108+
"root": {
109+
// Decoded root metadata object
110+
},
111+
"targets": {
112+
// Decoded targets metadata object
113+
}
114+
}
115+
```
116+
- **Error Response** (400/500):
117+
```json
118+
{
119+
"error": "Error message",
120+
"code": 400,
121+
"details": "Detailed error information"
122+
}
123+
```
124+
125+
#### POST `/metadata-single`
126+
127+
Retrieves and decodes a single metadata blob from a specific commit in a remote repository.
97128

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

152+
#### POST `/policy-query`
153+
154+
Evaluates a branch/path query against policy metadata at a specific remote policy commit.
155+
156+
- **Request Body** (JSON):
157+
```json
158+
{
159+
"url": "https://github.com/user/repo.git",
160+
"commit": "abc123...",
161+
"branch": "main",
162+
"changedPath": "src/app.ts"
163+
}
164+
```
165+
- **Response** (200 OK):
166+
```json
167+
{
168+
"matchedBranch": "main",
169+
"matchedRule": "src/**",
170+
"requiredApprovals": 2,
171+
"authorizedUsers": ["alice", "bob"]
172+
}
173+
```
174+
- **Error Response** (400/500):
175+
```json
176+
{
177+
"error": "Error message",
178+
"code": 400,
179+
"details": "Detailed error information"
180+
}
181+
```
182+
121183
---
122184

123185
### Local Repository Endpoints
124186

125187
#### POST `/commits-local`
126188

127-
Lists commits from the HEAD of a local Git repository.
189+
Lists commits from the `refs/gittuf/policy` ref of a local Git repository.
128190

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

155217
#### POST `/metadata-local`
156218

157-
Retrieves and decodes a metadata blob from a specific commit in a local repository.
219+
Retrieves and decodes both `root.json` and `targets.json` from a specific commit in a local repository.
220+
221+
- **Request Body** (JSON):
222+
```json
223+
{
224+
"path": "/path/to/local/repo",
225+
"commit": "abc123..."
226+
}
227+
```
228+
- **Response** (200 OK):
229+
```json
230+
{
231+
"root": {
232+
// Decoded root metadata object
233+
},
234+
"targets": {
235+
// Decoded targets metadata object
236+
}
237+
}
238+
```
239+
- **Error Response** (400/500):
240+
```json
241+
{
242+
"error": "Error message",
243+
"code": 400,
244+
"details": "Detailed error information"
245+
}
246+
```
247+
248+
#### POST `/metadata-local-single`
249+
250+
Retrieves and decodes a single metadata blob from a specific commit in a local repository.
158251

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

275+
#### POST `/policy-query-local`
276+
277+
Evaluates a branch/path query against policy metadata at a specific local policy commit.
278+
279+
- **Request Body** (JSON):
280+
```json
281+
{
282+
"path": "/path/to/local/repo",
283+
"commit": "abc123...",
284+
"branch": "main",
285+
"changedPath": "src/app.ts"
286+
}
287+
```
288+
- **Response** (200 OK):
289+
```json
290+
{
291+
"matchedBranch": "main",
292+
"matchedRule": "src/**",
293+
"requiredApprovals": 2,
294+
"authorizedUsers": ["alice", "bob"]
295+
}
296+
```
297+
- **Error Response** (400/500):
298+
```json
299+
{
300+
"error": "Error message",
301+
"code": 400,
302+
"details": "Detailed error information"
303+
}
304+
```
305+
182306
## Project Structure
183307

184308
```
@@ -194,4 +318,4 @@ go-backend/
194318
├── go.mod # Go module definition and dependencies
195319
├── go.sum # Dependencies checksums
196320
└── .env # Environment configuration
197-
```
321+
```

go-backend/cmd/server/main.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,24 @@ func main() {
3838
config.AllowHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization"}
3939
router.Use(cors.New(config))
4040

41+
// health check endpoint
42+
router.GET("/health", handlers.Health)
43+
4144
// Remote repository endpoints
4245
router.POST("/commits", handlers.ListCommits)
4346
router.POST("/metadata", handlers.GetMetadata)
47+
router.POST("/metadata-single", handlers.GetMetadataSingle)
48+
router.POST("/policy-query", handlers.QueryPolicyRemote)
4449

4550
// Local repository endpoints
4651
router.POST("/commits-local", handlers.ListCommitsLocal)
4752
router.POST("/metadata-local", handlers.GetMetadataLocal)
53+
router.POST("/metadata-local-single", handlers.GetMetadataLocalSingle)
54+
router.POST("/policy-query-local", handlers.QueryPolicyLocal)
4855

4956
port := os.Getenv("PORT")
5057
if port == "" {
51-
port = "5000"
58+
port = "8080"
5259
}
5360

5461
if err := router.Run(":" + port); err != nil {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright The gittuf Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package handlers
5+
6+
import (
7+
"net/http"
8+
9+
"github.com/gin-gonic/gin"
10+
)
11+
12+
// simple health check endpoint for the backend server
13+
func Health(c *gin.Context) {
14+
c.JSON(http.StatusOK, gin.H{
15+
"status": "Looks good!",
16+
})
17+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright The gittuf Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package handlers
5+
6+
import (
7+
"errors"
8+
"fmt"
9+
"net/http"
10+
"strings"
11+
12+
"github.com/gittuf/visualizer/go-backend/internal/logger"
13+
"github.com/gittuf/visualizer/go-backend/internal/models"
14+
"github.com/gittuf/visualizer/go-backend/internal/services"
15+
"github.com/gittuf/visualizer/go-backend/internal/validation"
16+
"github.com/go-git/go-git/v5/plumbing/object"
17+
18+
"github.com/gin-gonic/gin"
19+
)
20+
21+
func loadRemoteRepo(c *gin.Context, url, endpoint string) (string, func(), bool) {
22+
repoPath, cleanup, err := services.CloneAndFetchRepo(url)
23+
if err != nil {
24+
logger.Sugar.Errorf("Exception in %s: %v", endpoint, err)
25+
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
26+
Error: "Failed to clone or fetch repository",
27+
Code: http.StatusInternalServerError,
28+
Details: err.Error(),
29+
})
30+
return "", nil, false
31+
}
32+
33+
return repoPath, cleanup, true
34+
}
35+
36+
func validateLocalRepoPath(c *gin.Context, path string) (string, bool) {
37+
absPath, err := validation.GetAbsolutePath(path)
38+
if err != nil {
39+
c.JSON(http.StatusBadRequest, models.ErrorResponse{
40+
Error: "Invalid path",
41+
Code: http.StatusBadRequest,
42+
Details: err.Error(),
43+
})
44+
return "", false
45+
}
46+
47+
if !validation.PathExists(absPath) {
48+
c.JSON(http.StatusBadRequest, models.ErrorResponse{
49+
Error: fmt.Sprintf("Path does not exist: %s", absPath),
50+
Code: http.StatusBadRequest,
51+
Details: absPath,
52+
})
53+
return "", false
54+
}
55+
56+
if !validation.IsValidGitRepo(absPath) {
57+
c.JSON(http.StatusBadRequest, models.ErrorResponse{
58+
Error: fmt.Sprintf("Not a valid Git repository: %s", absPath),
59+
Code: http.StatusBadRequest,
60+
Details: absPath,
61+
})
62+
return "", false
63+
}
64+
65+
return absPath, true
66+
}
67+
68+
func writeBindError(c *gin.Context, err error) {
69+
c.JSON(http.StatusBadRequest, models.ErrorResponse{
70+
Error: "Invalid request body",
71+
Code: http.StatusBadRequest,
72+
Details: err.Error(),
73+
})
74+
}
75+
76+
func requireFields(c *gin.Context, fields ...string) bool {
77+
missing := make([]string, 0, len(fields)/2)
78+
for i := 0; i+1 < len(fields); i += 2 {
79+
if fields[i+1] == "" {
80+
missing = append(missing, fmt.Sprintf("'%s'", fields[i]))
81+
}
82+
}
83+
if len(missing) == 0 {
84+
return true
85+
}
86+
87+
c.JSON(http.StatusBadRequest, models.ErrorResponse{
88+
Error: fmt.Sprintf("Missing required field(s): %s", strings.Join(missing, ", ")),
89+
Code: http.StatusBadRequest,
90+
})
91+
return false
92+
}
93+
94+
func writeMetadataError(c *gin.Context, action string, err error) {
95+
status := http.StatusInternalServerError
96+
if errors.Is(err, object.ErrFileNotFound) {
97+
status = http.StatusNotFound
98+
}
99+
100+
c.JSON(status, models.ErrorResponse{
101+
Error: action,
102+
Code: status,
103+
Details: err.Error(),
104+
})
105+
}

0 commit comments

Comments
 (0)