Skip to content

chore(api): generated docs #2197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 27, 2025
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
10 changes: 10 additions & 0 deletions api/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
SHELL := /bin/bash

.PHONY: swagger
swagger:
swag fmt -g api.go
swag init -g api.go

.PHONY: swag
swag:
which swag || (go install github.com/swaggo/swag/cmd/swag@latest)
30 changes: 30 additions & 0 deletions api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ Defines the core data structures and types used throughout the API. This include
- Custom error types
- Shared interfaces

#### `/docs`
Contains Swagger-generated API documentation. This includes:
- API endpoint definitions
- Request/response schemas
- Authentication methods
- API operation descriptions

#### `/pkg`
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.

Expand Down Expand Up @@ -82,3 +89,26 @@ Provides a client library for interacting with the API. The client package imple
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.

For integration examples and usage patterns, refer to the integration tests in the `/integration` directory.

## Generating the Docs

The API documentation is generated using Swagger. To generate or update the docs:

1. Ensure the `swag` tool is installed:
```
make swag
```

2. Generate the Swagger documentation:
```
make swagger
```

This will scan the codebase for Swagger annotations and generate the API documentation files in the `/docs` directory.

Once the API is running, the Swagger documentation is available at the endpoint:
```
/api/swagger/
```

You can use this interactive documentation to explore the available endpoints, understand request/response formats, and test API operations directly from your browser.
33 changes: 27 additions & 6 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,34 @@ import (
"net/http"

"github.com/gorilla/mux"
"github.com/sirupsen/logrus"

"github.com/replicatedhq/embedded-cluster/api/controllers/auth"
"github.com/replicatedhq/embedded-cluster/api/controllers/console"
"github.com/replicatedhq/embedded-cluster/api/controllers/install"
_ "github.com/replicatedhq/embedded-cluster/api/docs"
"github.com/replicatedhq/embedded-cluster/api/types"
"github.com/sirupsen/logrus"
httpSwagger "github.com/swaggo/http-swagger/v2"
)

// @title Embedded Cluster API
// @version 0.1
// @description This is the API for the Embedded Cluster project.
// @termsOfService http://swagger.io/terms/

// @contact.name API Support
// @contact.url https://github.com/replicatedhq/embedded-cluster/issues
// @contact.email [email protected]

// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html

// @host localhost:30080
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this needs to be dynamic

// @BasePath /api

// @securityDefinitions.basic BasicAuth
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is incorrect

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we remove it then?


// @externalDocs.description OpenAPI
// @externalDocs.url https://swagger.io/resources/open-api/
type API struct {
authController auth.Controller
consoleController console.Controller
Expand Down Expand Up @@ -94,11 +114,12 @@ func New(password string, opts ...APIOption) (*API, error) {

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

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

authenticatedRouter := router.PathPrefix("").Subrouter()
authenticatedRouter := router.PathPrefix("/").Subrouter()
authenticatedRouter.Use(a.authMiddleware)

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

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

func (a *API) JSONError(w http.ResponseWriter, r *http.Request, err error) {
func (a *API) jsonError(w http.ResponseWriter, r *http.Request, err error) {
var apiErr *types.APIError
if !errors.As(err, &apiErr) {
apiErr = types.NewInternalServerError(err)
Expand All @@ -133,7 +154,7 @@ func (a *API) JSONError(w http.ResponseWriter, r *http.Request, err error) {
response, err := json.Marshal(apiErr)
if err != nil {
a.logError(r, err, "failed to encode response")
w.WriteHeader(http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

Expand Down
14 changes: 7 additions & 7 deletions api/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ func (a *API) authMiddleware(next http.Handler) http.Handler {
if token == "" {
err := errors.New("authorization header is required")
a.logError(r, err, "failed to authenticate")
types.NewUnauthorizedError(err).JSON(w)
a.jsonError(w, r, types.NewUnauthorizedError(err))
return
}

if !strings.HasPrefix(token, "Bearer ") {
err := errors.New("authorization header must start with Bearer ")
a.logError(r, err, "failed to authenticate")
types.NewUnauthorizedError(err).JSON(w)
a.jsonError(w, r, types.NewUnauthorizedError(err))
return
}

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

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

token, err := a.authController.Authenticate(r.Context(), request.Password)
if errors.Is(err, auth.ErrInvalidPassword) {
types.NewUnauthorizedError(err).JSON(w)
a.jsonError(w, r, types.NewUnauthorizedError(err))
return
}

if err != nil {
a.logError(r, err, "failed to authenticate")
types.NewInternalServerError(err).JSON(w)
a.jsonError(w, r, types.NewInternalServerError(err))
return
}

response := AuthResponse{
Token: token,
}

json.NewEncoder(w).Encode(response)
a.json(w, r, http.StatusOK, response)
}
8 changes: 4 additions & 4 deletions api/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ func (a *API) getBranding(w http.ResponseWriter, r *http.Request) {
branding, err := a.consoleController.GetBranding()
if err != nil {
a.logError(r, err, "failed to get branding")
a.JSONError(w, r, err)
a.jsonError(w, r, err)
return
}

response := getBrandingResponse{
Branding: branding,
}

a.JSON(w, r, http.StatusOK, response)
a.json(w, r, http.StatusOK, response)
}

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

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

a.JSON(w, r, http.StatusOK, response)
a.json(w, r, http.StatusOK, response)
}
88 changes: 88 additions & 0 deletions api/docs/docs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Package docs Code generated by swaggo/swag. DO NOT EDIT
package docs

import "github.com/swaggo/swag"

const docTemplate = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"termsOfService": "http://swagger.io/terms/",
"contact": {
"name": "API Support",
"url": "https://github.com/replicatedhq/embedded-cluster/issues",
"email": "[email protected]"
},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/health": {
"get": {
"description": "get the health of the API",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"health"
],
"summary": "Get the health of the API",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/types.Health"
}
}
}
}
}
},
"definitions": {
"types.Health": {
"type": "object",
"properties": {
"status": {
"type": "string"
}
}
}
},
"securityDefinitions": {
"BasicAuth": {
"type": "basic"
}
},
"externalDocs": {
"description": "OpenAPI",
"url": "https://swagger.io/resources/open-api/"
}
}`

// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "0.1",
Host: "localhost:30080",
BasePath: "/api",
Schemes: []string{},
Title: "Embedded Cluster API",
Description: "This is the API for the Embedded Cluster project.",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
RightDelim: "}}",
}

func init() {
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}
64 changes: 64 additions & 0 deletions api/docs/swagger.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"swagger": "2.0",
"info": {
"description": "This is the API for the Embedded Cluster project.",
"title": "Embedded Cluster API",
"termsOfService": "http://swagger.io/terms/",
"contact": {
"name": "API Support",
"url": "https://github.com/replicatedhq/embedded-cluster/issues",
"email": "[email protected]"
},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "0.1"
},
"host": "localhost:30080",
"basePath": "/api",
"paths": {
"/health": {
"get": {
"description": "get the health of the API",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"health"
],
"summary": "Get the health of the API",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/types.Health"
}
}
}
}
}
},
"definitions": {
"types.Health": {
"type": "object",
"properties": {
"status": {
"type": "string"
}
}
}
},
"securityDefinitions": {
"BasicAuth": {
"type": "basic"
}
},
"externalDocs": {
"description": "OpenAPI",
"url": "https://swagger.io/resources/open-api/"
}
}
Loading
Loading