docs: improve swagger

- use markdown files in swagger
- automate version, use swager.conf template
- embed swagger ui index.html as docs.html
This commit is contained in:
André Roth
2024-10-05 15:26:16 +02:00
parent 2df82e87b7
commit 69a1e2561d
10 changed files with 341 additions and 19 deletions

5
.gitignore vendored
View File

@@ -70,4 +70,7 @@ usr/bin/aptly
dpkgs/
debian/changelog.dpkg-bak
docs/
docs/docs.go
docs/swagger.json
docs/swagger.yaml
docs/swagger.conf

View File

@@ -45,6 +45,9 @@ version: ## Print aptly version
swagger-install:
# Install swag
@test -f $(BINPATH)/swag || GOOS=linux GOARCH=amd64 go install github.com/swaggo/swag/cmd/swag@latest
# Generate swagger.conf
cp docs/swagger.conf.tpl docs/swagger.conf
echo "// @version $(VERSION)" >> docs/swagger.conf
azurite-start:
azurite & \
@@ -55,7 +58,7 @@ azurite-stop:
swagger: swagger-install
# Generate swagger docs
@PATH=$(BINPATH)/:$(PATH) swag init --markdownFiles docs
@PATH=$(BINPATH)/:$(PATH) swag init --markdownFiles docs --generalInfo docs/swagger.conf
etcd-install:
# Install etcd
@@ -107,7 +110,7 @@ serve: prepare swagger-install ## Run development server (auto recompiling)
test -f $(BINPATH)/air || go install github.com/air-verse/air@v1.52.3
cp debian/aptly.conf ~/.aptly.conf
sed -i /enableSwaggerEndpoint/s/false/true/ ~/.aptly.conf
PATH=$(BINPATH):$$PATH air -build.pre_cmd 'swag init -q --markdownFiles docs' -build.exclude_dir docs,system,debian,pgp/keyrings,pgp/test-bins,completion.d,man,deb/testdata,console,_man,systemd,obj-x86_64-linux-gnu -- api serve -listen 0.0.0.0:3142
PATH=$(BINPATH):$$PATH air -build.pre_cmd 'swag init -q --markdownFiles docs --generalInfo docs/swagger.conf' -build.exclude_dir docs,system,debian,pgp/keyrings,pgp/test-bins,completion.d,man,deb/testdata,console,_man,systemd,obj-x86_64-linux-gnu -- api serve -listen 0.0.0.0:3142
dpkg: prepare swagger ## Build debian packages
@test -n "$(DEBARCH)" || (echo "please define DEBARCH"; exit 1)
@@ -205,7 +208,8 @@ man: ## Create man pages
clean: ## remove local build and module cache
# Clean all generated and build files
test -d .go/ && chmod u+w -R .go/ && rm -rf .go/ || true
find .go/ -type d ! -perm -u=w -exec chmod u+w {} \;
rm -rf .go/
rm -rf build/ obj-*-linux-gnu* tmp/
rm -f unit.out aptly.test VERSION docs/docs.go docs/swagger.json docs/swagger.yaml docs/swagger.conf
find system/ -type d -name __pycache__ -exec rm -rf {} \; 2>/dev/null || true

View File

@@ -12,7 +12,7 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/rs/zerolog/log"
_ "github.com/aptly-dev/aptly/docs" // import docs
"github.com/aptly-dev/aptly/docs"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
@@ -27,26 +27,22 @@ func apiMetricsGet() gin.HandlerFunc {
}
func redirectSwagger(c *gin.Context) {
if c.Request.URL.Path == "/docs/index.html" {
c.Redirect(http.StatusMovedPermanently, "/docs.html")
return
}
if c.Request.URL.Path == "/docs/" {
c.Redirect(http.StatusMovedPermanently, "/docs/index.html")
c.Redirect(http.StatusMovedPermanently, "/docs.html")
return
}
if c.Request.URL.Path == "/docs" {
c.Redirect(http.StatusMovedPermanently, "/docs.html")
return
}
c.Next()
}
// Router returns prebuilt with routes http.Handler
// @title Aptly API
// @version 1.0
// @description Aptly REST API Documentation
// @contact.name Aptly
// @contact.url http://github.com/aptly-dev/aptly
// @contact.email support@aptly.info
// @license.name MIT License
// @license.url http://www.
// @BasePath /api
func Router(c *ctx.AptlyContext) http.Handler {
if aptly.EnableDebug {
gin.SetMode(gin.DebugMode)
@@ -73,6 +69,9 @@ func Router(c *ctx.AptlyContext) http.Handler {
router.Use(gin.Recovery(), gin.ErrorLogger())
if c.Config().EnableSwaggerEndpoint {
router.GET("docs.html", func(c *gin.Context) {
c.Data(http.StatusOK, "text/html; charset=utf-8", docs.DocsHTML)
})
router.Use(redirectSwagger)
url := ginSwagger.URL("/docs/doc.json")
router.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))

1
docs/Database.md Normal file
View File

@@ -0,0 +1 @@
# Aptly Database Operations

29
docs/Publish.md Normal file
View File

@@ -0,0 +1,29 @@
# Aptly Publish Points
<div>
Publish snapshot or local repo as Debian repository which could be
served by HTTP/FTP/rsync server. Repository is signed by user's key with
GnuPG. Key should be created beforehand (see section GPG Keys below).
Published repository could be consumed directly by apt.
Repositories could be published to Amazon S3 service: create bucket,
[configure publishing endpoint](/doc/feature/s3/) and use S3 endpoint when
publishing.
#### GPG Keys
GPG key is required to sign any published repository. Key should be
generated before publishing first repository.
Key generation, storage, backup and revocation is out of scope of this
document, there are many tutorials available, e.g. [this one](http://fedoraproject.org/wiki/Creating_GPG_Keys).
Publiс part of the key should be exported from your keyring using `gpg --export --armor` and
imported into apt keyring using `apt-key` tool on all machines that would be using published
repositories.
Signing releases is highly recommended, but if you want to skip it, you
can either use `gpgDisableSign` configuration option or `--skip-signing`
flag.
</div>

10
docs/Status.md Normal file
View File

@@ -0,0 +1,10 @@
# Aptly Status Information
<div>
## Something
Very interesting ... asd wer
</div>

112
docs/api.md Normal file
View File

@@ -0,0 +1,112 @@
Using aptly via REST API allows to achieve two goals:
1. Remote access to aptly service: e.g. uploading packages and publishing them from CI server.
2. Concurrent access to aptly by multiple users.
#### Quickstart
Run `aptly api serve` to start HTTP service:
$ aptly api serve
Starting web server at: :8080 (press Ctrl+C to quit)...
[GIN-debug] GET /api/version --> github.com/aptly-dev/aptly/api.apiVersion (4 handlers)
...
By default aptly would listen on `:8080`, but it could be changed with `-listen` flag.
Usage:
$ aptly api serve -listen=:8080
Flags:
- `-listen=":8080"`: host:port for HTTP listening
- `-no-lock`: don't lock the database
When `-no-lock` option is enabled, API server acquires and drops the lock
around all the operations, so that API and CLI could be used concurrently.
Try some APIs:
$ curl http://localhost:8080/api/version
{"Version":"0.9~dev"}
$ curl -F file=@aptly_0.8_i386.deb http://localhost:8080/api/files/aptly_0.8
["aptly_0.8/aptly_0.8_i386.deb"]
$ curl -X POST -H 'Content-Type: application/json' --data '{"Name": "aptly-repo"}' http://localhost:8080/api/repos
{"Name":"aptly-repo","Comment":"","DefaultDistribution":"","DefaultComponent":""}
$ curl -X POST http://localhost:8080/api/repos/aptly-repo/file/aptly_0.8
{"failedFiles":[],"report":{"warnings":[],"added":["aptly_0.8_i386 added"],"removed":[]}}
$ curl http://localhost:8080/api/repos/aptly-repo/packages
["Pi386 aptly 0.8 966561016b44ed80"]
$ curl -X POST -H 'Content-Type: application/json' --data '{"Distribution": "wheezy", "Sources": [{"Name": "aptly-repo"}]}' http://localhost:8080/api/publish//repos
{"Architectures":["i386"],"Distribution":"wheezy","Label":"","Origin":"","Prefix":".","SourceKind":"local","Sources":[{"Component":"main","Name":"aptly-repo"}],"Storage":""}
#### Security
For security reasons it is advised to let Aptly listen on a Unix domain socket rather than a port. Sockets are subject to file permissions and thus allow for user-level access control while binding to a port only gives host-level access control. To use a socket simply run Aptly with a suitable listen flag such as `aptly api serve -listen=unix:///var/run/aptly.sock`.
Aptly's HTTP API shouldn't be directly exposed to the Internet: there's no authentication/protection of APIs. To publish the API it could be proxied through a HTTP proxy or server (e.g. nginx) to add an authentication mechanism or disallow all non-GET requests. [Reference example](https://github.com/sepich/nginx-ldap) for LDAP based per-repo access with nginx.
#### Notes
1. Some things (for example, S3 publishing endpoints) could be set up only using configuration file, so it requires restart of aptly HTTP server in order for changes to take effect.
1. GPG key passphrase can't be input on console, so either passwordless GPG keys are required or passphrase should be specified in API parameters.
1. Some script might be required to start/stop aptly HTTP service.
1. Some parameters are given as part of URLs, which requires proper url encoding. Unfortunately, at the moment it's not possible to pass URL arguments with `/` in them.
### How to implement equivalent of aptly commands using API
* `aptly mirror`: [mirror API](/doc/api/mirror)
* `list`: list
* `create`: create
* `drop`: delete
* `show`: show
* `search`: show packages/search
* `update`: update mirror
* `aptly repo`: [local repos API](/doc/api/repos)
* `add`: [file upload API](/doc/api/files) + add packages from uploaded file
* `copy`: show packages/search + add packages by key
* `create`: create
* `drop`: delete
* `edit`: edit
* `import`: not available, as mirror API is not implemented yet
* `list`: list
* `move`: show packages/search + add packages by key + delete packages by key
* `remove`: show packages/search + delete packages by key
* `rename`: not available yet, should be part of edit API
* `search`: show packages/search
* `show`: show
* `aptly snapshot`: [snapshots API](/doc/api/snapshots)
* `create`:
* empty: create snapshot with empty package references
* from mirror: not available yet
* from local repo: create snapshot from local repo
* `diff`: snapshot difference API
* `drop`: delete
* `filter`: show packages/search + create snapshot from package references
* `list`: list
* `merge`: show packages/search + processing + create snapshot from package references
* `pull`: show packages/search + processing + create snapshot from package references (might be implemented as API in future versions)
* `rename`: edit
* `search`: show packages/search
* `show`: show
* `verify`: not available yet
* `aptly publish`: [publish API](/doc/api/publish)
* `drop`: delete
* `list`: list
* `repo`: publish repo
* `snapshot`: publish snapshot
* `switch`: switch/update
* `update`: switch/update
* `aptly package`: [packages API](/doc/api/packages)
* `search`: not available yet
* `show`: only one format, with package key as input
* `aptly graph`: [graph API](/doc/api/misc)
* `aptly version`: [version API](/doc/api/misc)
* `aptly db`: not available yet

142
docs/docs.html Normal file
View File

@@ -0,0 +1,142 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="/docs/swagger-ui.css" >
<link rel="icon" type="image/png" href="/docs/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="/docs/favicon-16x16.png" sizes="16x16" />
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body {
margin:0;
background: #fafafa;
}
/* no json link */
.swagger-ui a.link {
display: none;
}
/* API Summary */
.swagger-ui .opblock-summary-description {
flex: 0 0 auto !important;
margin-left: auto !important;
margin-right: 2em;
font-weight: bold;
font-size: 14pt !important;
}
/* Tag Group */
.swagger-ui .opblock-tag-section > .opblock-tag > a {
color: grey !important;
align-self: flex-start;
font-size: 14pt;
width: 7em;
}
/* */
.swagger-ui .opblock-tag-section .opblock-tag .markdown > h1 {
font-size: 16pt;
}
/* Show Tag Group description only if opened */
.swagger-ui .opblock-tag-section:not(.is-open) .opblock-tag .markdown > div {
display: none;
}
/* Keep open button on top */
.swagger-ui .expand-operation {
align-self: flex-start;
}
</style>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0">
<defs>
<symbol viewBox="0 0 20 20" id="unlocked">
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path>
</symbol>
<symbol viewBox="0 0 20 20" id="locked">
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z"/>
</symbol>
<symbol viewBox="0 0 20 20" id="close">
<path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z"/>
</symbol>
<symbol viewBox="0 0 20 20" id="large-arrow">
<path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z"/>
</symbol>
<symbol viewBox="0 0 20 20" id="large-arrow-down">
<path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"/>
</symbol>
<symbol viewBox="0 0 24 24" id="jump-to">
<path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"/>
</symbol>
<symbol viewBox="0 0 24 24" id="expand">
<path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>
</symbol>
</defs>
</svg>
<div id="swagger-ui"></div>
<script src="/docs/swagger-ui-bundle.js"> </script>
<script src="/docs/swagger-ui-standalone-preset.js"> </script>
<script>
window.onload = function() {
const ui = SwaggerUIBundle({
url: "\/docs\/doc.json",
dom_id: '#swagger-ui',
validatorUrl: null,
oauth2RedirectUrl: `${window.location.protocol}//${window.location.host}${window.location.pathname.split('/').slice(0, window.location.pathname.split('/').length - 1).join('/')}/oauth2-redirect.html`,
persistAuthorization: false ,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset.slice(1) // remote topbar
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
docExpansion: "list",
deepLinking: true ,
defaultModelsExpandDepth: 5
})
const defaultClientId = "";
if (defaultClientId) {
ui.initOAuth({
clientId: defaultClientId
})
}
window.ui = ui
}
</script>
</body>
</html>

View File

@@ -1,3 +1,9 @@
package docs
import _ "github.com/swaggo/swag" // make sure swag is in go.mod
import (
_ "embed"
_ "github.com/swaggo/swag" // make sure swag is in go.mod
)
//go:embed docs.html
var DocsHTML []byte

16
docs/swagger.conf.tpl Normal file
View File

@@ -0,0 +1,16 @@
package docs
// @title Aptly API
// @description.markdown
// @contact.name Aptly
// @contact.url http://github.com/aptly-dev/aptly
// @Tag.name Publish
// @Tag.description.markdown
// @Tag.name Database
// @Tag.description.markdown
// @Tag.name Status
// @Tag.description.markdown
// version will be appended here: