Skip to content

Commit 7515add

Browse files
authored
Merge pull request #58 from OpenCHAMI/synackd/openapi
doc: add godoc comments and reflect tags for OpenAPI parsing
2 parents 32c4631 + d4ffc77 commit 7515add

File tree

17 files changed

+2681
-82
lines changed

17 files changed

+2681
-82
lines changed

.github/workflows/Release.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ jobs:
4141
echo "GO_VERSION=$(go version | awk '{print $3}')" >> $GITHUB_ENV
4242
echo "BUILD_USER=$(whoami)" >> $GITHUB_ENV
4343
echo "CGO_ENABLED=0" >> $GITHUB_ENV
44+
45+
- name: Install Swag
46+
run: go install github.com/swaggo/swag/cmd/swag@latest
47+
4448
- name: Release with goreleaser
4549
uses: goreleaser/goreleaser-action@v6
4650
env:

.goreleaser.yaml

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ before:
55
hooks:
66
# You may remove this if you don't use go modules.
77
- go mod tidy
8+
- swag init -g cmd/cloud-init-server/main.go
89

910
builds:
1011
- id: cloud-init
@@ -15,15 +16,15 @@ builds:
1516
# export GO_VERSION=$(go version | awk '{print $3}')
1617
# export BUILD_USER=$(whoami)
1718
ldflags:
18-
- "-X main.GitCommit={{.Commit}} \
19-
-X main.BuildTime={{.Timestamp}} \
20-
-X main.Version={{.Version}} \
21-
-X main.GitBranch={{.Branch}} \
22-
-X main.GitTag={{.Tag}} \
23-
-X main.GitState={{ .Env.GIT_STATE }} \
24-
-X main.BuildHost={{ .Env.BUILD_HOST }} \
25-
-X main.GoVersion={{ .Env.GO_VERSION }} \
26-
-X main.BuildUser={{ .Env.BUILD_USER }} "
19+
- "-X 'main.GitCommit={{.Commit}}' \
20+
-X 'main.BuildTime={{.Timestamp}}' \
21+
-X 'main.Version={{.Version}}' \
22+
-X 'main.GitBranch={{.Branch}}' \
23+
-X 'main.GitTag={{.Tag}}' \
24+
-X 'main.GitState={{ .Env.GIT_STATE }}' \
25+
-X 'main.BuildHost={{ .Env.BUILD_HOST }}' \
26+
-X 'main.GoVersion={{ .Env.GO_VERSION }}' \
27+
-X 'main.BuildUser={{ .Env.BUILD_USER }}'"
2728
goos:
2829
- linux
2930
- darwin

cmd/cloud-init-server/group_handlers.go

Lines changed: 70 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ import (
99
yaml "gopkg.in/yaml.v2"
1010
)
1111

12+
// GetGroups godoc
13+
//
14+
// @Summary Get groups known by cloud-init
15+
// @Description Get meta-data and cloud-init config for all groups known to
16+
// @Description cloud-init. Note that group membership is managed outside of
17+
// @Description the cloud-init service, normally in SMD.
18+
// @Tags admin,groups
19+
// @Produce json
20+
// @Success 200 {object} map[string]cistore.ClusterDefaults
21+
// @Failure 500 {object} nil
22+
// @Router /cloud-init/admin/groups [get]
1223
func (h CiHandler) GetGroups(w http.ResponseWriter, r *http.Request) {
1324
var (
1425
groups map[string]cistore.GroupData
@@ -27,37 +38,28 @@ func (h CiHandler) GetGroups(w http.ResponseWriter, r *http.Request) {
2738
}
2839
}
2940

30-
/*
31-
AddGroupHandler adds a new group with it's associated data specified by the user.
32-
33-
*/
34-
// AddGroupHandler handles the HTTP request for adding a new group.
35-
// It parses the request data into a GroupData struct, validates it,
36-
// and then attempts to store it using the handler's store. If successful,
37-
// it sets the Location header to the new group's URL and responds with
38-
// HTTP status 201 Created. If there is an error during parsing or storing,
39-
// it responds with the appropriate HTTP error status.
40-
//
41-
// Curl Example:
41+
// AddGroupHandler godoc
4242
//
43-
// curl -X POST http://localhost:27777/cloud-init/admin/groups/ \
44-
// -H "Content-Type: application/json" \
45-
// -d '{
46-
// "name": "x3000",
47-
// "description": "Cabinet x3000",
48-
// "data": {
49-
// "syslog_aggregator": "192.168.0.1"
50-
// },
51-
// "file": {
52-
// "content": "#cloud-config\nrsyslog:\n remotes: {x3000: \"192.168.0.5\"}\nservice_reload_command: auto\n",
53-
// "encoding": "plain"
54-
// }
55-
// }'
56-
// It parses the request data into a GroupData struct and attempts to add it to the store.
57-
// Encoding options are "plain" or "base64".
58-
// If parsing fails, it responds with a 422 Unprocessable Entity status.
59-
// If adding the group data to the store fails, it responds with a 409 Conflict status.
60-
// On success, it sets the Location header to the new group's URL and responds with a 201 Created status.
43+
// @Summary Add a new group
44+
// @Description Add a new group to cloud-init corresponding to an SMD group.
45+
// @Description Group-wide meta-data and/or a cloud-init configuration (in
46+
// @Description either plain or base64 encoding) can be specified.
47+
// @Description
48+
// @Description If successful, a 201 Created status is returned and the
49+
// @Description `Location` header is set to the new group's groups endpoint,
50+
// @Description `/groups/{name}`.
51+
// @Description
52+
// @Description If request parsing fails, a 422 Unprocessable Entity status is
53+
// @Description returned. If adding group data to the data store fails, a 409
54+
// @Description Conflict status is returned.
55+
// @Tags admin,groups
56+
// @Accept json
57+
// @Success 201 {object} nil
58+
// @Failure 409 {object} nil
59+
// @Failure 422 {object} nil
60+
// @Header 201 {string} Location "/groups/{id}"
61+
// @Param group body cistore.GroupData true "Group data"
62+
// @Router /cloud-init/admin/groups [post]
6163
func (h CiHandler) AddGroupHandler(w http.ResponseWriter, r *http.Request) {
6264
var (
6365
data cistore.GroupData
@@ -80,6 +82,17 @@ func (h CiHandler) AddGroupHandler(w http.ResponseWriter, r *http.Request) {
8082

8183
}
8284

85+
// GetGroupHandler godoc
86+
//
87+
// @Summary Get data for single group
88+
// @Description Get meta-data and cloud-init config for a single group known to
89+
// @Description cloud-init.
90+
// @Tags admin,groups
91+
// @Produce json
92+
// @Success 200 {object} cistore.GroupData
93+
// @Failure 500 {object} nil
94+
// @Param id path string true "Group ID"
95+
// @Router /cloud-init/admin/groups/{id} [get]
8396
func (h CiHandler) GetGroupHandler(w http.ResponseWriter, r *http.Request) {
8497
var (
8598
id string = chi.URLParam(r, "id")
@@ -102,6 +115,24 @@ func (h CiHandler) GetGroupHandler(w http.ResponseWriter, r *http.Request) {
102115
w.Write(bytes)
103116
}
104117

118+
// UpdateGroupHandler godoc
119+
//
120+
// @Summary Set group-specific meta-data and/or cloud-init config
121+
// @Description Set meta-data or cloud-init configuration for a specific group,
122+
// @Description overwriting any previous values.
123+
// @Description
124+
// @Description If successful, a 201 Created status is returned and the
125+
// @Description `Location` header is set to the new group's groups endpoint,
126+
// @Description `/groups/{group}`. This operation is idempotent and replaces
127+
// @Description any existing content.
128+
// @Tags admin,groups
129+
// @Accept json
130+
// @Success 201 {object} nil
131+
// @Failure 422 {object} nil
132+
// @Failure 500 {object} nil
133+
// @Header 201 {string} Location "/groups/{name}"
134+
// @Param name path string true "Group name"
135+
// @Router /cloud-init/admin/groups/{name} [put]
105136
func (h CiHandler) UpdateGroupHandler(w http.ResponseWriter, r *http.Request) {
106137
var (
107138
groupName string = chi.URLParam(r, "name")
@@ -125,6 +156,15 @@ func (h CiHandler) UpdateGroupHandler(w http.ResponseWriter, r *http.Request) {
125156
w.WriteHeader(http.StatusCreated)
126157
}
127158

159+
// RemoveGroupHandler godoc
160+
//
161+
// @Summary Delete a group
162+
// @Description Delete a group with its meta-data and cloud-init config.
163+
// @Tags admin,groups
164+
// @Success 200 {object} nil
165+
// @Failure 500 {object} nil
166+
// @Param id path string true "Group ID"
167+
// @Router /cloud-init/admin/groups/{id} [delete]
128168
func (h CiHandler) RemoveGroupHandler(w http.ResponseWriter, r *http.Request) {
129169
var (
130170
id string = chi.URLParam(r, "id")

cmd/cloud-init-server/handlers.go

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ import (
55
"io"
66
"net/http"
77

8+
// Import to run swag.Register() to generated docs
9+
_ "github.com/OpenCHAMI/cloud-init/docs"
810
"github.com/OpenCHAMI/cloud-init/internal/smdclient"
911
"github.com/OpenCHAMI/cloud-init/pkg/cistore"
1012
"github.com/OpenCHAMI/cloud-init/pkg/wgtunnel"
1113
"github.com/go-chi/chi/v5"
1214
"github.com/rs/zerolog/log"
15+
"github.com/swaggo/swag"
1316
)
1417

1518
type CiHandler struct {
@@ -44,6 +47,34 @@ func parseData(r *http.Request) (cistore.GroupData, error) {
4447
return data, nil
4548
}
4649

50+
// DocsHandler godoc
51+
//
52+
// @Summary Return JSON-formatted OpenAPI documentation
53+
// @Produce json
54+
// @Success 200 {object} string
55+
// @Failure 500 {object} nil
56+
// @Router /cloud-init/openapi.json [get]
57+
func DocsHandler(w http.ResponseWriter, r *http.Request) {
58+
doc, err := swag.ReadDoc()
59+
if err != nil {
60+
log.Error().Msgf("Error reading OpenAPI docs: %v", err)
61+
w.WriteHeader(http.StatusInternalServerError)
62+
return
63+
}
64+
bDoc := []byte(doc)
65+
w.Write(bDoc)
66+
}
67+
68+
// SetClusterDataHandler godoc
69+
//
70+
// @Summary Set cluster defaults
71+
// @Description Set default meta-data values for cluster.
72+
// @Tags admin,cluster-defaults
73+
// @Accept json
74+
// @Success 201 {object} nil
75+
// @Failure 400 {object} nil
76+
// @Failure 500 {object} nil
77+
// @Router /cloud-init/admin/cluster-defaults [post]
4778
func SetClusterDataHandler(store cistore.Store) http.HandlerFunc {
4879
return func(w http.ResponseWriter, r *http.Request) {
4980

@@ -73,6 +104,15 @@ func SetClusterDataHandler(store cistore.Store) http.HandlerFunc {
73104
}
74105
}
75106

107+
// GetClusterDataHandler godoc
108+
//
109+
// @Summary Get cluster defaults
110+
// @Description Get default meta-data values for cluster.
111+
// @Tags admin,cluster-defaults
112+
// @Produce json
113+
// @Success 200 {object} cistore.ClusterDefaults
114+
// @Failure 500 {object} nil
115+
// @Router /cloud-init/admin/cluster-defaults [get]
76116
func GetClusterDataHandler(store cistore.Store) http.HandlerFunc {
77117
return func(w http.ResponseWriter, r *http.Request) {
78118
data, err := store.GetClusterDefaults()
@@ -95,6 +135,17 @@ func GetClusterDataHandler(store cistore.Store) http.HandlerFunc {
95135
}
96136
}
97137

138+
// InstanceInfoHandler godoc
139+
//
140+
// @Summary Set node-specific meta-data
141+
// @Description Set meta-data for a specific node ID, overwriting relevant group meta-data.
142+
// @Tags admin,instance-data
143+
// @Accept json
144+
// @Success 201 {object} nil
145+
// @Failure 400 {object} nil
146+
// @Failure 500 {object} nil
147+
// @Param id path string true "Node ID"
148+
// @Router /cloud-init/admin/instance-info/{id} [put]
98149
func InstanceInfoHandler(sm smdclient.SMDClientInterface, store cistore.Store) http.HandlerFunc {
99150
return func(w http.ResponseWriter, r *http.Request) {
100151
if r.Method != http.MethodPut {
@@ -129,7 +180,25 @@ func InstanceInfoHandler(sm smdclient.SMDClientInterface, store cistore.Store) h
129180
}
130181
}
131182

132-
// Phone home should be a POST request x-www-form-urlencoded like this: pub_key_rsa=rsa_contents&pub_key_ecdsa=ecdsa_contents&pub_key_ed25519=ed25519_contents&instance_id=i-87018aed&hostname=myhost&fqdn=myhost.internal
183+
// PhoneHomeHandler godoc
184+
//
185+
// @Summary Signal to cloud-init server that host has completed running cloud-init configuration
186+
// @Description Signal to the cloud-init server that the specific host has completed running
187+
// @Description the cloud-init configuration tasks so that, if a WireGuard tunnel is being used,
188+
// @Description it can be torn down. This endpoint should not be manually requested by a user
189+
// @Description but is only meant to be used by a cloud-init client that has received its
190+
// @Description config from an OpenCHAMI cloud-init server.
191+
// @Tags phone-home
192+
// @Success 200 {object} nil
193+
// @Failure 400 {object} nil
194+
// @Param id path string true "Node's unique identifier"
195+
// @Param pub_key_rsa formData string true "Node's WireGuard RSA public key"
196+
// @Param pub_key_ecdsa formData string true "Node's WireGuard ECDSA public key"
197+
// @Param pub_key_ed25519 formData string true "Node's WireGuard ED35519 public key"
198+
// @Param instance_id formData string true "Node's given instance ID"
199+
// @Param hostname formData string true "Node's given hostname"
200+
// @Param fqdn formData string true "Node's given fully-qualified domain name"
201+
// @Router /cloud-init/phone-home/{id} [post]
133202
func PhoneHomeHandler(wg *wgtunnel.InterfaceManager, sm smdclient.SMDClientInterface) http.HandlerFunc {
134203
return func(w http.ResponseWriter, r *http.Request) {
135204
if r.Method != http.MethodPost {

cmd/cloud-init-server/main.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
package main
22

3+
// @Title OpenCHAMI Cloud-Init Server API
4+
// @Version 1.0.0
5+
// @Description API for cloud-init clients using the OpenCHAMI cloud-init server
6+
// @License.name MIT
7+
// @License.url https://github.com/OpenCHAMI/.github/blob/main/LICENSE
8+
39
import (
410
"flag"
511
"fmt"
@@ -187,6 +193,7 @@ func main() {
187193

188194
func initCiClientRouter(router chi.Router, handler *CiHandler, wgInterfaceManager *wgtunnel.InterfaceManager) {
189195
// Add cloud-init endpoints to router
196+
router.Get("/openapi.json", DocsHandler)
190197
router.With(wireGuardMiddleware).Get("/user-data", UserDataHandler)
191198
router.With(wireGuardMiddleware).Get("/meta-data", MetaDataHandler(handler.sm, handler.store))
192199
router.With(wireGuardMiddleware).Get("/vendor-data", VendorDataHandler(handler.sm, handler.store))

cmd/cloud-init-server/metadata.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import (
99

1010
type MetaData struct {
1111
InstanceID string `json:"instance-id" yaml:"instance-id"`
12-
LocalHostname string `json:"local-hostname" yaml:"local-hostname"`
13-
Hostname string `json:"hostname" yaml:"hostname"`
14-
ClusterName string `json:"cluster-name" yaml:"cluster-name"`
12+
LocalHostname string `json:"local-hostname" yaml:"local-hostname" example:"compute-1" description:"Node-specific short hostname"`
13+
Hostname string `json:"hostname" yaml:"hostname" example:"compute-1.demo.openchami.cluster" description:"Node-specific hostname, often FQDN and how other hosts may reference this host"`
14+
ClusterName string `json:"cluster-name" yaml:"cluster-name" example:"demo" description:"Long name of entire cluster, used as a human-readable identifier and is used in the cluster's FQDN"`
1515
InstanceData InstanceData `json:"instance-data" yaml:"instance_data"`
1616
}
1717

@@ -40,8 +40,8 @@ type VendorData struct {
4040
SubRole string `json:"sub-role,omitempty" yaml:"sub_role,omitempty"`
4141
Cabinet string `json:"cabinet,omitempty" yaml:"cabinet,omitempty"`
4242
Location string `json:"location,omitempty" yaml:"location,omitempty"`
43-
ClusterName string `json:"cluster_name,omitempty" yaml:"cluster_name,omitempty"`
44-
Groups map[string]Group `json:"groups" yaml:"groups"`
43+
ClusterName string `json:"cluster_name,omitempty" yaml:"cluster_name,omitempty" example:"demo" description:"Long name of entire cluster, used as a human-readable identifier and is used in the cluster's FQDN"`
44+
Groups map[string]Group `json:"groups" yaml:"groups" description:"Groups known to cloud-init and their meta-data"`
4545
}
4646

4747
type Group map[string]interface{}
@@ -128,11 +128,11 @@ func generateHostname(clusterName string, shortName string, nidLength int, comp
128128
} else {
129129
sname = shortName
130130
}
131-
if nidLength == 0 {
131+
if nidLength == 0 {
132132
nlen = 4
133133
} else {
134134
nlen = nidLength
135135
}
136136
log.Debug().Msgf("shortName: %v, nidLength: %v", sname, nlen)
137-
return fmt.Sprintf("%s%0*d",sname, nlen, nid)
137+
return fmt.Sprintf("%s%0*d", sname, nlen, nid)
138138
}

cmd/cloud-init-server/metadata_handlers.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,22 @@ func getActualRequestIP(r *http.Request) string {
2828
return strings.TrimSpace(ip)
2929
}
3030

31+
// MetaDataHandler godoc
32+
//
33+
// @Summary Get meta-data for requesting node
34+
// @Description Get meta-data for requesting node based on the requesting IP.
35+
// @Description
36+
// @Description If the impersonation API is enabled, an ID can be provided in
37+
// @Description the URL path using `/admin/impersonation`. In this case, the
38+
// @Description meta-data will be retrieved for the requested ID.
39+
// @Produce application/x-yaml
40+
// @Success 200 {object} MetaData
41+
// @Failure 404 {object} nil
42+
// @Failure 422 {object} nil
43+
// @Failure 500 {object} nil
44+
// @Param id path string false "Node ID"
45+
// @Router /cloud-init/meta-data [get]
46+
// @Router /cloud-init/admin/impersonation/{id}/meta-data [get]
3147
func MetaDataHandler(smd smdclient.SMDClientInterface, store cistore.Store) http.HandlerFunc {
3248
return func(w http.ResponseWriter, r *http.Request) {
3349
var urlId string = chi.URLParam(r, "id")

0 commit comments

Comments
 (0)