diff --git a/README.md b/README.md index 22955eb6..e1322736 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ The objective of this project is to provide a REST API service for the Aerospike The service is written in Go and wraps the [asbackup/asrestore](https://github.com/aerospike/aerospike-tools-backup) tools, built as shared libraries. +Use the [OpenApi generation script](./scripts/generate_OpenApi.sh) to generate OpenApi Specification for the service. + ## C Library known issues * Not thread-safe * Uncaptured stdout logs diff --git a/internal/server/server.go b/internal/server/server.go index 5b3b6a85..58b7a70b 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -29,6 +29,17 @@ type HTTPServer struct { backupBackends map[string]service.BackupBackend } +// Annotations to generate OpenAPI description (https://github.com/swaggo/swag) +// @title Backup Service REST API Specification +// @version 0.1.0 +// @description Aerospike Backup Service +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html +// @host localhost:8080 +// +// @externalDocs.description OpenAPI +// @externalDocs.url https://swagger.io/resources/open-api/ +// // NewHTTPServer returns a new instance of HTTPServer. func NewHTTPServer(host string, port int, backends []service.BackupBackend, config *model.Config) *HTTPServer { @@ -113,6 +124,9 @@ func (ws *HTTPServer) Shutdown() error { return ws.server.Shutdown(context.Background()) } +// @Summary Root endpoint +// @Router / [get] +// @Success 200 "" func rootActionHandler(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { w.WriteHeader(http.StatusNotFound) @@ -120,6 +134,9 @@ func rootActionHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "") } +// @Summary Returns the configuration the service started with in the JSON format. +// @Router /config [get] +// @Success 200 {array} model.Config func (ws *HTTPServer) configActionHandler(w http.ResponseWriter, _ *http.Request) { configuration, err := json.MarshalIndent(ws.config, "", " ") // pretty print if err != nil { @@ -128,18 +145,33 @@ func (ws *HTTPServer) configActionHandler(w http.ResponseWriter, _ *http.Request fmt.Fprint(w, string(configuration)) } +// @Summary Health endpoint. +// @Router /health [get] +// @Success 200 "Ok" func healthActionHandler(w http.ResponseWriter, _ *http.Request) { fmt.Fprintf(w, "Ok") } +// @Summary Readiness endpoint. +// @Router /ready [get] +// @Success 200 "Ok" func readyActionHandler(w http.ResponseWriter, _ *http.Request) { fmt.Fprintf(w, "Ok") } +// @Summary Returns application version. +// @Router /version [get] +// @Success 200 {string} string "version" func versionActionHandler(w http.ResponseWriter, _ *http.Request) { fmt.Fprint(w, util.Version) } +// @Summary Trigger an asynchronous restore operation. +// @Description Specify the directory parameter for the full backup restore. +// @Description Use the file parameter to restore from an incremental backup file. +// @Router /restore [post] +// @Param request body model.RestoreRequest true "query params" +// @Success 200 {integer} int "Job ID (int64)" func (ws *HTTPServer) restoreHandler(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost { var request model.RestoreRequest @@ -161,6 +193,11 @@ func (ws *HTTPServer) restoreHandler(w http.ResponseWriter, r *http.Request) { } } +// @Summary Retrieve status for a restore job. +// @Produce plain +// @Param jobId query int true "Job ID to retrieve the status" +// @Router /restore/status [get] +// @Success 200 {string} string "Job status" func (ws *HTTPServer) restoreStatusHandler(w http.ResponseWriter, r *http.Request) { jobIDParam := r.URL.Query().Get("jobId") jobID, err := strconv.Atoi(jobIDParam) @@ -171,6 +208,12 @@ func (ws *HTTPServer) restoreStatusHandler(w http.ResponseWriter, r *http.Reques } } +// @Summary Get available full backups. +// @Produce plain +// @Param name query string true "Backup policy name" +// @Router /backup/full/list [get] +// @Success 200 {array} model.BackupDetails "Full backups" +// @Failure 404 {string} string "" func (ws *HTTPServer) getAvailableFullBackups(w http.ResponseWriter, r *http.Request) { policyName := r.URL.Query().Get("name") if policyName == "" { @@ -192,6 +235,12 @@ func (ws *HTTPServer) getAvailableFullBackups(w http.ResponseWriter, r *http.Req } } +// @Summary Get available incremental backups. +// @Produce plain +// @Param name query string true "Backup policy name" +// @Router /backup/incremental/list [get] +// @Success 200 {array} model.BackupDetails "Incremental backups" +// @Failure 404 {string} string "" func (ws *HTTPServer) getAvailableIncrBackups(w http.ResponseWriter, r *http.Request) { policyName := r.URL.Query().Get("name") if policyName == "" { diff --git a/pkg/service/backup_backend_local.go b/pkg/service/backup_backend_local.go index 9a971535..64e1b044 100644 --- a/pkg/service/backup_backend_local.go +++ b/pkg/service/backup_backend_local.go @@ -24,14 +24,9 @@ var _ BackupBackend = (*BackupBackendLocal)(nil) // NewBackupBackendLocal returns a new BackupBackendLocal instance. func NewBackupBackendLocal(path, backupPolicyName string) *BackupBackendLocal { - if err := os.Chmod(path, 0744); err != nil { - slog.Warn("Failed to Chmod backup directory", "path", path, "err", err) - } + prepareDirectory(path) incrDirectoryPath := path + "/" + incremenalBackupDirectory - if err := os.Mkdir(incrDirectoryPath, 0744); err != nil { - slog.Debug("Failed to Mkdir incremental backup directory", - "path", incrDirectoryPath, "err", err) - } + prepareDirectory(incrDirectoryPath) return &BackupBackendLocal{ path: path, stateFilePath: path + "/" + stateFileName, @@ -39,6 +34,18 @@ func NewBackupBackendLocal(path, backupPolicyName string) *BackupBackendLocal { } } +func prepareDirectory(path string) { + _, err := os.Stat(path) + if os.IsNotExist(err) { + if err = os.Mkdir(path, 0744); err != nil { + slog.Warn("Error creating backup directory", "path", path, "err", err) + } + } + if err = os.Chmod(path, 0744); err != nil { + slog.Warn("Failed to Chmod backup directory", "path", path, "err", err) + } +} + func (local *BackupBackendLocal) readState() *model.BackupState { bytes, err := os.ReadFile(local.stateFilePath) state := model.NewBackupState() diff --git a/scripts/generate_OpenApi.sh b/scripts/generate_OpenApi.sh new file mode 100755 index 00000000..4d19f728 --- /dev/null +++ b/scripts/generate_OpenApi.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Check if the "swag" command exists +if ! command -v swag &> /dev/null +then + echo "swag is not installed. Installing..." + + # Install swag + go install github.com/swaggo/swag/cmd/swag@latest + + # Check if the installation was successful + if [ $? -eq 0 ] + then + echo "swag installed successfully." + else + echo "Error: Failed to install swag. Please install it manually." + exit 1 + fi +fi + +ROOT_PATH=$(cd `dirname $0` && pwd)/.. +swag init -d $ROOT_PATH/internal/server,$ROOT_PATH/pkg/model \ + -g server.go \ + -o $ROOT_PATH/docs