-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.go
320 lines (300 loc) · 10.2 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"
"strings"
"github.com/pb33f/libopenapi"
base "github.com/pb33f/libopenapi/datamodel/high/base"
v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
orderedmap "github.com/pb33f/libopenapi/orderedmap"
log "github.com/sirupsen/logrus"
)
// Generic struct for sonarqube API calls, based on ingestion the sonarqube API list view output to https://mholt.github.io/json-to-go
type AutoGenerated struct {
WebServices []struct {
Path string `json:"path"`
Since string `json:"since,omitempty"`
Description string `json:"description,omitempty"`
Actions []struct {
Key string `json:"key"`
Description string `json:"description"`
Since string `json:"since"`
DeprecatedSince string `json:"deprecatedSince,omitempty"`
Internal bool `json:"internal"`
Post bool `json:"post"`
HasResponseExample bool `json:"hasResponseExample"`
Changelog []struct {
Description string `json:"description"`
Version string `json:"version"`
} `json:"changelog"`
Params []struct {
Key string `json:"key"`
Description string `json:"description"`
Required bool `json:"required"`
Internal bool `json:"internal"`
MaximumLength int `json:"maximumLength,omitempty"`
Since string `json:"since,omitempty"`
} `json:"params"`
} `json:"actions"`
} `json:"webServices"`
}
// try to catch the version
func fetchSonarVersion(base string) string {
// default version
var version string = "Unknowns Edition"
var basenameURL string
if string(base[len(base)-1:]) == "/" {
basenameURL = base + "api/server/version"
} else {
basenameURL = base + "/api/server/version"
}
resp, err := http.Get(basenameURL)
if err == nil && resp.StatusCode == 200 {
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
version = string(body[:])
} else {
log.Debug("Retrieving version returned error: ", err)
}
log.Debug("Using version number: ", version)
return version
}
func processSonarqubeOpenAPI(basenameOAS string, basenameV1 string) libopenapi.Document {
// Try to read the V1 spec as a file
rawData, err := os.ReadFile(basenameV1)
if err != nil {
// try to read the OAS as a link
log.Debug("Error reading ", basenameV1, " as file .... trying as URL")
var basenameURL string
if string(basenameV1[len(basenameV1)-1:]) == "/" {
basenameURL = basenameV1 + "api/webservices/list"
} else {
basenameURL = basenameV1 + "/api/webservices/list"
}
resp, err := http.Get(basenameURL)
if err != nil {
log.Fatal("Neither file nor url when fetching: ", basenameURL)
} else {
if resp.StatusCode == 200 {
defer resp.Body.Close()
var body []byte
body, _ = io.ReadAll(resp.Body)
rawData = body
log.Info("Reading URL successfully, size ", len(rawData))
} else {
log.Fatal("Received status code: ", resp.StatusCode, " when fetching URL: ", basenameURL)
}
}
} else {
log.Info("Reading file successfully, size ", len(rawData))
}
// Try to read the OAS as a file
log.Debug("Reading OAS data")
rawDataOAS, err := os.ReadFile(basenameOAS)
if err != nil {
// try to read the OAS as a link
log.Debug("Error reading ", basenameOAS, " as file .... trying as URL")
var basenameURL string
if string(basenameOAS[len(basenameOAS)-1:]) == "/" {
basenameURL = basenameOAS + "api/v2/api-docs"
} else {
basenameURL = basenameOAS + "/api/v2/api-docs"
}
resp, err := http.Get(basenameURL)
if err != nil {
log.Fatal("Neither file nor url when fetching: ", basenameURL)
} else {
if resp.StatusCode == 200 {
defer resp.Body.Close()
var body []byte
body, _ = io.ReadAll(resp.Body)
rawDataOAS = body
log.Debug("Reading : ", len(rawDataOAS))
} else {
log.Fatal("Received status code: ", resp.StatusCode, " when fetching URL: ", basenameURL)
}
}
}
log.Debug("Generating NewDocument from OAS data")
document, err := libopenapi.NewDocument(rawDataOAS)
if err != nil {
log.Fatal(fmt.Sprintf("cannot create new document: %e", err))
}
v3Model, errors := document.BuildV3Model()
v3Model.Model.Info.Version = fetchSonarVersion(basenameV1)
if len(errors) > 0 {
for i := range errors {
log.Warning(fmt.Sprintf("error: %e\n", errors[i]))
}
log.Fatal(fmt.Sprintf("cannot create v3 model from document: %d errors reported",
len(errors)))
}
// Reprocess the v3Model to rename all the path
for apiPathName, _ := range v3Model.Model.Index.GetAllPaths() {
// fmt.Println(i, j)
path, ok := v3Model.Model.Paths.PathItems.Delete(apiPathName)
if !ok {
log.Debug("Path exist but not found: ", apiPathName)
}
operationPath := fmt.Sprintf("/v2%s", apiPathName)
v3Model.Model.Paths.PathItems.Set(operationPath, path)
}
// Update servers considering new path
// var newServers []*v3.Server
for _, server := range v3Model.Model.Servers {
url := strings.Replace(server.URL, "/api/v2", "", -1)
server.URL = url
}
// Add a simple string schema for query params
stringSchema := base.CreateSchemaProxy(&base.Schema{
Type: []string{"string"},
})
// create an ordered property map
propMap := orderedmap.New[string, *base.SchemaProxy]()
// create a sample/empty schema that we can use in our document
nothingSchema := base.CreateSchemaProxy(&base.Schema{
Type: []string{"object"},
Properties: propMap,
})
// create response map
responseMap := orderedmap.New[string, *v3.Response]()
// OK response
okResponseContentMap := orderedmap.New[string, *v3.MediaType]()
okResponseContentMap.Set("application/json", &v3.MediaType{
Schema: nothingSchema,
})
// add responses
responseMap.Set("2XX", &v3.Response{
Description: "Successful query.",
Content: okResponseContentMap,
})
var bodyContentMap AutoGenerated
var wsActionPost string = "post"
var operationDeprecated bool = true
json.Unmarshal(rawData, &bodyContentMap)
for _, webService := range bodyContentMap.WebServices {
for _, wsAction := range webService.Actions {
var operationParameters []*v3.Parameter
for _, actionParam := range wsAction.Params {
operationParameter := v3.Parameter{
Description: actionParam.Description,
AllowEmptyValue: actionParam.Required,
In: "query",
Required: &actionParam.Required,
Name: actionParam.Key,
Schema: stringSchema,
}
operationParameters = append(operationParameters, &operationParameter)
}
extDoc := base.ExternalDoc{
Description: "Sonarqube Documentation for " + wsAction.Key,
URL: fmt.Sprintf("https://next.sonarqube.com/sonarqube/web_api/api/%s/%s", webService.Path, wsAction.Key),
}
// var operationResponse v3.Response
if wsAction.Post {
wsActionPost = "get"
} else {
wsActionPost = "post"
}
if wsAction.DeprecatedSince == "" {
operationDeprecated = false
} else {
operationDeprecated = true
}
operation := &v3.Operation{
Description: wsAction.Description,
OperationId: strings.Replace(fmt.Sprintf("v1_%s_%s_%s", wsActionPost, webService.Path, wsAction.Key), "/", "_", -1),
Summary: strings.Replace(wsAction.Key, "_", " ", -1),
ExternalDocs: &extDoc,
Parameters: operationParameters,
Deprecated: &operationDeprecated,
Responses: &v3.Responses{
Codes: responseMap,
},
}
operationPath := fmt.Sprintf("/%s/%s", webService.Path, wsAction.Key)
var newPath *v3.PathItem
if wsAction.Post {
newPath = &v3.PathItem{
Description: webService.Description,
Post: operation,
}
} else {
newPath = &v3.PathItem{
Description: webService.Description,
Get: operation,
}
}
v3Model.Model.Paths.PathItems.Set(operationPath, newPath)
}
}
log.Debug("Finished processing document.")
return document
}
func main() {
var inputV1Flag, inputV2Flag, outFile string
var debug, urlFlagPublic, urlFlagDefault bool
flag.BoolVar(&debug, "debug", false, "Debug")
flag.StringVar(&inputV1Flag, "inputv1", "", "Name of the sonarqube webservices export file from /api/webservices/list or Sonarqube base URL.")
flag.StringVar(&inputV2Flag, "inputv2", "", "Name of the sonarqube webservices export file from /api/v2/api-docs or Sonarqube base URL. (if missing we reuse the inputv1 value.)")
flag.StringVar(&outFile, "outfile", "", "File base name to save to (extension will be added)")
flag.BoolVar(&urlFlagDefault, "defaulturl", false, "Use http://localhost:9000/ for the url.")
flag.BoolVar(&urlFlagPublic, "publicurl", false, "Use https://next.sonarqube.com/sonarqube/ for the url.")
// flag.BoolVar(&jsonOutput, "json", false, "Generate JSON instead of YAML.")
flag.Parse()
if debug {
log.SetLevel(log.DebugLevel)
}
if urlFlagDefault {
inputV1Flag = "http://localhost:9000/"
inputV2Flag = inputV1Flag
log.Debug("Using ", inputV1Flag, " for inputV1 and inputV2")
}
if urlFlagPublic {
inputV1Flag = "https://next.sonarqube.com/sonarqube/"
inputV2Flag = inputV1Flag
log.Debug("Using ", inputV1Flag, " for inputV1 and inputV2")
}
if inputV2Flag == "" {
inputV2Flag = inputV1Flag
log.Debug("Using ", inputV1Flag, " for inputV1 and inputV2")
} else {
log.Debug("Using ", inputV1Flag, " for inputV1 and ", inputV1Flag, " inputV2")
}
sonarqubeDocument := processSonarqubeOpenAPI(inputV2Flag, inputV1Flag)
sonarqubeModel, errors := sonarqubeDocument.BuildV3Model()
if len(errors) > 0 {
for i := range errors {
log.Debug(fmt.Sprintf("error: %e\n", errors[i]))
}
log.Fatal(fmt.Sprintf("cannot create v3 model from document: %d errors reported",
len(errors)))
}
var renderDocument []byte
log.Debug("Writing to file:", outFile)
if outFile != "" {
if strings.HasSuffix(strings.ToLower(outFile), "json") {
renderDocument, _ = sonarqubeModel.Model.RenderJSON(" ")
} else {
renderDocument, _ = sonarqubeModel.Model.Render()
}
f, err := os.Create(outFile)
if err != nil {
log.Fatal("Issues when creating file: ", outFile)
}
defer f.Close()
_, err = f.Write(renderDocument)
if err != nil {
log.Fatal("Error writing file: ", err)
}
log.Info("Saving content to file: ", outFile)
} else {
renderDocument, _ = sonarqubeModel.Model.Render()
fmt.Println(string(renderDocument))
}
os.Exit(0)
}