Skip to content

Commit 331ea0d

Browse files
authored
chore(api): generated docs (#2197)
1 parent 0e6a734 commit 331ea0d

File tree

14 files changed

+375
-89
lines changed

14 files changed

+375
-89
lines changed

api/Makefile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
SHELL := /bin/bash
2+
3+
.PHONY: swagger
4+
swagger:
5+
swag fmt -g api.go
6+
swag init -g api.go
7+
8+
.PHONY: swag
9+
swag:
10+
which swag || (go install github.com/swaggo/swag/cmd/swag@latest)

api/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ Defines the core data structures and types used throughout the API. This include
1919
- Custom error types
2020
- Shared interfaces
2121

22+
#### `/docs`
23+
Contains Swagger-generated API documentation. This includes:
24+
- API endpoint definitions
25+
- Request/response schemas
26+
- Authentication methods
27+
- API operation descriptions
28+
2229
#### `/pkg`
2330
Contains shared utilities and helper packages that provide common functionality used across different parts of the API. This includes both general-purpose utilities and domain-specific helpers.
2431

@@ -82,3 +89,26 @@ Provides a client library for interacting with the API. The client package imple
8289
The API package is designed to be used as part of the larger Embedded Cluster system. It provides both HTTP endpoints for external access and a client library for internal use.
8390

8491
For integration examples and usage patterns, refer to the integration tests in the `/integration` directory.
92+
93+
## Generating the Docs
94+
95+
The API documentation is generated using Swagger. To generate or update the docs:
96+
97+
1. Ensure the `swag` tool is installed:
98+
```
99+
make swag
100+
```
101+
102+
2. Generate the Swagger documentation:
103+
```
104+
make swagger
105+
```
106+
107+
This will scan the codebase for Swagger annotations and generate the API documentation files in the `/docs` directory.
108+
109+
Once the API is running, the Swagger documentation is available at the endpoint:
110+
```
111+
/api/swagger/
112+
```
113+
114+
You can use this interactive documentation to explore the available endpoints, understand request/response formats, and test API operations directly from your browser.

api/api.go

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,34 @@ import (
77
"net/http"
88

99
"github.com/gorilla/mux"
10-
"github.com/sirupsen/logrus"
11-
1210
"github.com/replicatedhq/embedded-cluster/api/controllers/auth"
1311
"github.com/replicatedhq/embedded-cluster/api/controllers/console"
1412
"github.com/replicatedhq/embedded-cluster/api/controllers/install"
13+
_ "github.com/replicatedhq/embedded-cluster/api/docs"
1514
"github.com/replicatedhq/embedded-cluster/api/types"
15+
"github.com/sirupsen/logrus"
16+
httpSwagger "github.com/swaggo/http-swagger/v2"
1617
)
1718

19+
// @title Embedded Cluster API
20+
// @version 0.1
21+
// @description This is the API for the Embedded Cluster project.
22+
// @termsOfService http://swagger.io/terms/
23+
24+
// @contact.name API Support
25+
// @contact.url https://github.com/replicatedhq/embedded-cluster/issues
26+
// @contact.email support@replicated.com
27+
28+
// @license.name Apache 2.0
29+
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
30+
31+
// @host localhost:30080
32+
// @BasePath /api
33+
34+
// @securityDefinitions.basic BasicAuth
35+
36+
// @externalDocs.description OpenAPI
37+
// @externalDocs.url https://swagger.io/resources/open-api/
1838
type API struct {
1939
authController auth.Controller
2040
consoleController console.Controller
@@ -94,11 +114,12 @@ func New(password string, opts ...APIOption) (*API, error) {
94114

95115
func (a *API) RegisterRoutes(router *mux.Router) {
96116
router.HandleFunc("/health", a.getHealth).Methods("GET")
117+
router.PathPrefix("/swagger/").Handler(httpSwagger.WrapHandler)
97118

98119
router.HandleFunc("/auth/login", a.postAuthLogin).Methods("POST")
99120
router.HandleFunc("/branding", a.getBranding).Methods("GET")
100121

101-
authenticatedRouter := router.PathPrefix("").Subrouter()
122+
authenticatedRouter := router.PathPrefix("/").Subrouter()
102123
authenticatedRouter.Use(a.authMiddleware)
103124

104125
installRouter := authenticatedRouter.PathPrefix("/install").Subrouter()
@@ -111,7 +132,7 @@ func (a *API) RegisterRoutes(router *mux.Router) {
111132
consoleRouter.HandleFunc("/available-network-interfaces", a.getListAvailableNetworkInterfaces).Methods("GET")
112133
}
113134

114-
func (a *API) JSON(w http.ResponseWriter, r *http.Request, code int, payload any) {
135+
func (a *API) json(w http.ResponseWriter, r *http.Request, code int, payload any) {
115136
response, err := json.Marshal(payload)
116137
if err != nil {
117138
a.logError(r, err, "failed to encode response")
@@ -124,7 +145,7 @@ func (a *API) JSON(w http.ResponseWriter, r *http.Request, code int, payload any
124145
w.Write(response)
125146
}
126147

127-
func (a *API) JSONError(w http.ResponseWriter, r *http.Request, err error) {
148+
func (a *API) jsonError(w http.ResponseWriter, r *http.Request, err error) {
128149
var apiErr *types.APIError
129150
if !errors.As(err, &apiErr) {
130151
apiErr = types.NewInternalServerError(err)
@@ -133,7 +154,7 @@ func (a *API) JSONError(w http.ResponseWriter, r *http.Request, err error) {
133154
response, err := json.Marshal(apiErr)
134155
if err != nil {
135156
a.logError(r, err, "failed to encode response")
136-
w.WriteHeader(http.StatusInternalServerError)
157+
http.Error(w, err.Error(), http.StatusInternalServerError)
137158
return
138159
}
139160

api/auth.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ func (a *API) authMiddleware(next http.Handler) http.Handler {
2424
if token == "" {
2525
err := errors.New("authorization header is required")
2626
a.logError(r, err, "failed to authenticate")
27-
types.NewUnauthorizedError(err).JSON(w)
27+
a.jsonError(w, r, types.NewUnauthorizedError(err))
2828
return
2929
}
3030

3131
if !strings.HasPrefix(token, "Bearer ") {
3232
err := errors.New("authorization header must start with Bearer ")
3333
a.logError(r, err, "failed to authenticate")
34-
types.NewUnauthorizedError(err).JSON(w)
34+
a.jsonError(w, r, types.NewUnauthorizedError(err))
3535
return
3636
}
3737

@@ -40,7 +40,7 @@ func (a *API) authMiddleware(next http.Handler) http.Handler {
4040
err := a.authController.ValidateToken(r.Context(), token)
4141
if err != nil {
4242
a.logError(r, err, "failed to validate token")
43-
types.NewUnauthorizedError(err).JSON(w)
43+
a.jsonError(w, r, types.NewUnauthorizedError(err))
4444
return
4545
}
4646

@@ -53,25 +53,25 @@ func (a *API) postAuthLogin(w http.ResponseWriter, r *http.Request) {
5353
err := json.NewDecoder(r.Body).Decode(&request)
5454
if err != nil {
5555
a.logError(r, err, "failed to decode auth request")
56-
types.NewBadRequestError(err).JSON(w)
56+
a.jsonError(w, r, types.NewBadRequestError(err))
5757
return
5858
}
5959

6060
token, err := a.authController.Authenticate(r.Context(), request.Password)
6161
if errors.Is(err, auth.ErrInvalidPassword) {
62-
types.NewUnauthorizedError(err).JSON(w)
62+
a.jsonError(w, r, types.NewUnauthorizedError(err))
6363
return
6464
}
6565

6666
if err != nil {
6767
a.logError(r, err, "failed to authenticate")
68-
types.NewInternalServerError(err).JSON(w)
68+
a.jsonError(w, r, types.NewInternalServerError(err))
6969
return
7070
}
7171

7272
response := AuthResponse{
7373
Token: token,
7474
}
7575

76-
json.NewEncoder(w).Encode(response)
76+
a.json(w, r, http.StatusOK, response)
7777
}

api/console.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ func (a *API) getBranding(w http.ResponseWriter, r *http.Request) {
1414
branding, err := a.consoleController.GetBranding()
1515
if err != nil {
1616
a.logError(r, err, "failed to get branding")
17-
a.JSONError(w, r, err)
17+
a.jsonError(w, r, err)
1818
return
1919
}
2020

2121
response := getBrandingResponse{
2222
Branding: branding,
2323
}
2424

25-
a.JSON(w, r, http.StatusOK, response)
25+
a.json(w, r, http.StatusOK, response)
2626
}
2727

2828
type getListAvailableNetworkInterfacesResponse struct {
@@ -33,7 +33,7 @@ func (a *API) getListAvailableNetworkInterfaces(w http.ResponseWriter, r *http.R
3333
interfaces, err := a.consoleController.ListAvailableNetworkInterfaces()
3434
if err != nil {
3535
a.logError(r, err, "failed to list available network interfaces")
36-
a.JSONError(w, r, err)
36+
a.jsonError(w, r, err)
3737
return
3838
}
3939

@@ -45,5 +45,5 @@ func (a *API) getListAvailableNetworkInterfaces(w http.ResponseWriter, r *http.R
4545
NetworkInterfaces: interfaces,
4646
}
4747

48-
a.JSON(w, r, http.StatusOK, response)
48+
a.json(w, r, http.StatusOK, response)
4949
}

api/docs/docs.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Package docs Code generated by swaggo/swag. DO NOT EDIT
2+
package docs
3+
4+
import "github.com/swaggo/swag"
5+
6+
const docTemplate = `{
7+
"schemes": {{ marshal .Schemes }},
8+
"swagger": "2.0",
9+
"info": {
10+
"description": "{{escape .Description}}",
11+
"title": "{{.Title}}",
12+
"termsOfService": "http://swagger.io/terms/",
13+
"contact": {
14+
"name": "API Support",
15+
"url": "https://github.com/replicatedhq/embedded-cluster/issues",
16+
"email": "support@replicated.com"
17+
},
18+
"license": {
19+
"name": "Apache 2.0",
20+
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
21+
},
22+
"version": "{{.Version}}"
23+
},
24+
"host": "{{.Host}}",
25+
"basePath": "{{.BasePath}}",
26+
"paths": {
27+
"/health": {
28+
"get": {
29+
"description": "get the health of the API",
30+
"consumes": [
31+
"application/json"
32+
],
33+
"produces": [
34+
"application/json"
35+
],
36+
"tags": [
37+
"health"
38+
],
39+
"summary": "Get the health of the API",
40+
"responses": {
41+
"200": {
42+
"description": "OK",
43+
"schema": {
44+
"$ref": "#/definitions/types.Health"
45+
}
46+
}
47+
}
48+
}
49+
}
50+
},
51+
"definitions": {
52+
"types.Health": {
53+
"type": "object",
54+
"properties": {
55+
"status": {
56+
"type": "string"
57+
}
58+
}
59+
}
60+
},
61+
"securityDefinitions": {
62+
"BasicAuth": {
63+
"type": "basic"
64+
}
65+
},
66+
"externalDocs": {
67+
"description": "OpenAPI",
68+
"url": "https://swagger.io/resources/open-api/"
69+
}
70+
}`
71+
72+
// SwaggerInfo holds exported Swagger Info so clients can modify it
73+
var SwaggerInfo = &swag.Spec{
74+
Version: "0.1",
75+
Host: "localhost:30080",
76+
BasePath: "/api",
77+
Schemes: []string{},
78+
Title: "Embedded Cluster API",
79+
Description: "This is the API for the Embedded Cluster project.",
80+
InfoInstanceName: "swagger",
81+
SwaggerTemplate: docTemplate,
82+
LeftDelim: "{{",
83+
RightDelim: "}}",
84+
}
85+
86+
func init() {
87+
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
88+
}

api/docs/swagger.json

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"swagger": "2.0",
3+
"info": {
4+
"description": "This is the API for the Embedded Cluster project.",
5+
"title": "Embedded Cluster API",
6+
"termsOfService": "http://swagger.io/terms/",
7+
"contact": {
8+
"name": "API Support",
9+
"url": "https://github.com/replicatedhq/embedded-cluster/issues",
10+
"email": "support@replicated.com"
11+
},
12+
"license": {
13+
"name": "Apache 2.0",
14+
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
15+
},
16+
"version": "0.1"
17+
},
18+
"host": "localhost:30080",
19+
"basePath": "/api",
20+
"paths": {
21+
"/health": {
22+
"get": {
23+
"description": "get the health of the API",
24+
"consumes": [
25+
"application/json"
26+
],
27+
"produces": [
28+
"application/json"
29+
],
30+
"tags": [
31+
"health"
32+
],
33+
"summary": "Get the health of the API",
34+
"responses": {
35+
"200": {
36+
"description": "OK",
37+
"schema": {
38+
"$ref": "#/definitions/types.Health"
39+
}
40+
}
41+
}
42+
}
43+
}
44+
},
45+
"definitions": {
46+
"types.Health": {
47+
"type": "object",
48+
"properties": {
49+
"status": {
50+
"type": "string"
51+
}
52+
}
53+
}
54+
},
55+
"securityDefinitions": {
56+
"BasicAuth": {
57+
"type": "basic"
58+
}
59+
},
60+
"externalDocs": {
61+
"description": "OpenAPI",
62+
"url": "https://swagger.io/resources/open-api/"
63+
}
64+
}

0 commit comments

Comments
 (0)