Skip to content

Commit 91ae93a

Browse files
committed
support to load swaggers from an extension
1 parent f6fa6eb commit 91ae93a

File tree

11 files changed

+293
-66
lines changed

11 files changed

+293
-66
lines changed

cmd/extension.go

Lines changed: 59 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -17,75 +17,75 @@ limitations under the License.
1717
package cmd
1818

1919
import (
20-
"fmt"
21-
"io"
22-
"path/filepath"
23-
"runtime"
24-
"time"
20+
"fmt"
21+
"io"
22+
"path/filepath"
23+
"runtime"
24+
"time"
2525

26-
"github.com/linuxsuren/api-testing/pkg/downloader"
27-
"github.com/spf13/cobra"
26+
"github.com/linuxsuren/api-testing/pkg/downloader"
27+
"github.com/spf13/cobra"
2828
)
2929

3030
type extensionOption struct {
31-
ociDownloader downloader.PlatformAwareOCIDownloader
32-
output string
33-
registry string
34-
kind string
35-
tag string
36-
os string
37-
arch string
38-
timeout time.Duration
39-
imagePrefix string
31+
ociDownloader downloader.PlatformAwareOCIDownloader
32+
output string
33+
registry string
34+
kind string
35+
tag string
36+
os string
37+
arch string
38+
timeout time.Duration
39+
imagePrefix string
4040
}
4141

4242
func createExtensionCommand(ociDownloader downloader.PlatformAwareOCIDownloader) (c *cobra.Command) {
43-
opt := &extensionOption{
44-
ociDownloader: ociDownloader,
45-
}
46-
c = &cobra.Command{
47-
Use: "extension",
48-
Short: "Download extension binary files",
49-
Long: "Download the store extension files",
50-
Args: cobra.MinimumNArgs(1),
51-
RunE: opt.runE,
52-
}
53-
flags := c.Flags()
54-
flags.StringVarP(&opt.output, "output", "", ".", "The target directory")
55-
flags.StringVarP(&opt.tag, "tag", "", "", "The extension image tag, try to find the latest one if this is empty")
56-
flags.StringVarP(&opt.registry, "registry", "", "", "The target extension image registry, supported: docker.io, ghcr.io")
57-
flags.StringVarP(&opt.kind, "kind", "", "store", "The extension kind")
58-
flags.StringVarP(&opt.os, "os", "", runtime.GOOS, "The OS")
59-
flags.StringVarP(&opt.arch, "arch", "", runtime.GOARCH, "The architecture")
60-
flags.DurationVarP(&opt.timeout, "timeout", "", time.Minute, "The timeout of downloading")
61-
flags.StringVarP(&opt.imagePrefix, "image-prefix", "", "linuxsuren", "The prefix for the image address")
62-
return
43+
opt := &extensionOption{
44+
ociDownloader: ociDownloader,
45+
}
46+
c = &cobra.Command{
47+
Use: "extension",
48+
Short: "Download extension binary files",
49+
Long: "Download the store extension files",
50+
Args: cobra.MinimumNArgs(1),
51+
RunE: opt.runE,
52+
}
53+
flags := c.Flags()
54+
flags.StringVarP(&opt.output, "output", "", ".", "The target directory")
55+
flags.StringVarP(&opt.tag, "tag", "", "", "The extension image tag, try to find the latest one if this is empty")
56+
flags.StringVarP(&opt.registry, "registry", "", "", "The target extension image registry, supported: docker.io, ghcr.io")
57+
flags.StringVarP(&opt.kind, "kind", "", "store", "The extension kind")
58+
flags.StringVarP(&opt.os, "os", "", runtime.GOOS, "The OS")
59+
flags.StringVarP(&opt.arch, "arch", "", runtime.GOARCH, "The architecture")
60+
flags.DurationVarP(&opt.timeout, "timeout", "", time.Minute, "The timeout of downloading")
61+
flags.StringVarP(&opt.imagePrefix, "image-prefix", "", "linuxsuren", "The prefix for the image address")
62+
return
6363
}
6464

6565
func (o *extensionOption) runE(cmd *cobra.Command, args []string) (err error) {
66-
o.ociDownloader.WithOS(o.os)
67-
o.ociDownloader.WithArch(o.arch)
68-
o.ociDownloader.WithRegistry(o.registry)
69-
o.ociDownloader.WithImagePrefix(o.imagePrefix)
70-
o.ociDownloader.WithTimeout(o.timeout)
71-
o.ociDownloader.WithKind(o.kind)
72-
o.ociDownloader.WithContext(cmd.Context())
66+
o.ociDownloader.WithOS(o.os)
67+
o.ociDownloader.WithArch(o.arch)
68+
o.ociDownloader.WithRegistry(o.registry)
69+
o.ociDownloader.WithImagePrefix(o.imagePrefix)
70+
o.ociDownloader.WithTimeout(o.timeout)
71+
o.ociDownloader.WithKind(o.kind)
72+
o.ociDownloader.WithContext(cmd.Context())
7373

74-
for _, arg := range args {
75-
var reader io.Reader
76-
if reader, err = o.ociDownloader.Download(arg, o.tag, ""); err != nil {
77-
return
78-
} else if reader == nil {
79-
err = fmt.Errorf("cannot find %s", arg)
80-
return
81-
}
82-
extFile := o.ociDownloader.GetTargetFile()
83-
cmd.Println("found target file", extFile)
74+
for _, arg := range args {
75+
var reader io.Reader
76+
if reader, err = o.ociDownloader.Download(arg, o.tag, ""); err != nil {
77+
return
78+
} else if reader == nil {
79+
err = fmt.Errorf("cannot find %s", arg)
80+
return
81+
}
82+
extFile := o.ociDownloader.GetTargetFile()
83+
cmd.Println("found target file", extFile)
8484

85-
targetFile := filepath.Base(extFile)
86-
if err = downloader.WriteTo(reader, o.output, targetFile); err == nil {
87-
cmd.Println("downloaded", targetFile)
88-
}
89-
}
90-
return
85+
targetFile := filepath.Base(extFile)
86+
if err = downloader.WriteTo(reader, o.output, targetFile); err == nil {
87+
cmd.Println("downloaded", targetFile)
88+
}
89+
}
90+
return
9191
}

cmd/server.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"context"
2323
"errors"
2424
"fmt"
25+
"github.com/linuxsuren/api-testing/pkg/apispec"
2526
"net"
2627
"net/http"
2728
"os"
@@ -302,6 +303,15 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) {
302303
_ = o.httpServer.Shutdown(ctx)
303304
}()
304305

306+
go func() {
307+
err := apispec.DownloadSwaggerData("", extDownloader)
308+
if err != nil {
309+
fmt.Println("failed to download swagger data", err)
310+
} else {
311+
fmt.Println("success to download swagger data")
312+
}
313+
}()
314+
305315
mux := runtime.NewServeMux(runtime.WithMetadata(server.MetadataStoreFunc),
306316
runtime.WithMarshalerOption("application/json+pretty", &runtime.JSONPb{
307317
MarshalOptions: protojson.MarshalOptions{
@@ -342,6 +352,7 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) {
342352
mux.HandlePath(http.MethodGet, "/get", o.getAtestBinary)
343353
mux.HandlePath(http.MethodPost, "/runner/{suite}/{case}", service.WebRunnerHandler)
344354
mux.HandlePath(http.MethodGet, "/api/v1/sbom", service.SBomHandler)
355+
mux.HandlePath(http.MethodGet, "/api/v1/swaggers", apispec.SwaggersHandler)
345356

346357
postRequestProxyFunc := postRequestProxy(o.skyWalking)
347358
mux.HandlePath(http.MethodPost, "/browser/{app}", postRequestProxyFunc)

console/atest-ui/src/views/TestSuite.vue

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { reactive, ref, watch } from 'vue'
44
import { Edit, CopyDocument, Delete } from '@element-plus/icons-vue'
55
import type { FormInstance, FormRules } from 'element-plus'
66
import type { Suite, TestCase, Pair } from './types'
7-
import { NewSuggestedAPIsQuery, GetHTTPMethods } from './types'
7+
import { NewSuggestedAPIsQuery, GetHTTPMethods, SwaggerSuggestion } from './types'
88
import EditButton from '../components/EditButton.vue'
99
import { Cache } from './cache'
1010
import { useI18n } from 'vue-i18n'
@@ -20,6 +20,7 @@ const props = defineProps({
2020
})
2121
const emit = defineEmits(['updated'])
2222
let querySuggestedAPIs = NewSuggestedAPIsQuery(Cache.GetCurrentStore().name, props.name!)
23+
const querySwaggers = SwaggerSuggestion()
2324
2425
const suite = ref({
2526
name: '',
@@ -325,7 +326,10 @@ const renameTestSuite = (name: string) => {
325326
</el-select>
326327
</td>
327328
<td>
328-
<el-input class="mx-1" v-model="suite.spec.url" placeholder="API Spec URL"></el-input>
329+
<el-autocomplete
330+
v-model="suite.spec.url"
331+
:fetch-suggestions="querySwaggers"
332+
/>
329333
</td>
330334
</tr>
331335
</table>

console/atest-ui/src/views/net.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,12 @@ function GetSuggestedAPIs(name: string,
620620
.then(callback)
621621
}
622622

623+
function GetSwaggers(callback: (d: any) => void) {
624+
fetch(`/api/v1/swaggers`, {})
625+
.then(DefaultResponseProcess)
626+
.then(callback)
627+
}
628+
623629
function ReloadMockServer(config: any) {
624630
const requestOptions = {
625631
method: 'POST',
@@ -812,7 +818,7 @@ export const API = {
812818
CreateOrUpdateStore, GetStores, DeleteStore, VerifyStore,
813819
FunctionsQuery,
814820
GetSecrets, DeleteSecret, CreateOrUpdateSecret,
815-
GetSuggestedAPIs,
821+
GetSuggestedAPIs, GetSwaggers,
816822
ReloadMockServer, GetMockConfig, SBOM, DataQuery,
817823
getToken
818824
}

console/atest-ui/src/views/types.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,28 @@ export function NewSuggestedAPIsQuery(store: string, suite: string) {
102102
})
103103
}
104104
}
105+
106+
interface SwaggerItem {
107+
value: string
108+
}
109+
110+
export function SwaggerSuggestion() {
111+
return function (queryString: string, cb: (arg: any) => void) {
112+
API.GetSwaggers((e) => {
113+
var swaggers = [] as SwaggerItem[]
114+
e.forEach((item: string) => {
115+
swaggers.push({
116+
"value": `atest://${item}`
117+
})
118+
})
119+
120+
const results = queryString ? swaggers.filter((item: SwaggerItem) => {
121+
return item.value.toLowerCase().indexOf(queryString.toLowerCase()) != -1
122+
}) : swaggers
123+
cb(results.slice(0, 10))
124+
})
125+
}
126+
}
105127
export function CreateFilter(queryString: string) {
106128
return (v: Pair) => {
107129
return v.value.toLowerCase().indexOf(queryString.toLowerCase()) !== -1

pkg/apispec/remote_swagger.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
Copyright 2025 API Testing Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package apispec
18+
19+
import (
20+
"archive/tar"
21+
"compress/gzip"
22+
"encoding/json"
23+
"fmt"
24+
"github.com/linuxsuren/api-testing/pkg/downloader"
25+
"github.com/linuxsuren/api-testing/pkg/util/home"
26+
"io"
27+
"net/http"
28+
"os"
29+
"path/filepath"
30+
)
31+
32+
func DownloadSwaggerData(output string, dw downloader.PlatformAwareOCIDownloader) (err error) {
33+
dw.WithKind("data")
34+
dw.WithOS("")
35+
36+
var reader io.Reader
37+
if reader, err = dw.Download("swagger", "", ""); err != nil {
38+
return
39+
}
40+
41+
extFile := dw.GetTargetFile()
42+
43+
if output == "" {
44+
output = home.GetUserDataDir()
45+
}
46+
if err = os.MkdirAll(filepath.Dir(output), 0755); err != nil {
47+
return
48+
}
49+
50+
targetFile := filepath.Base(extFile)
51+
fmt.Println("start to save", filepath.Join(output, targetFile))
52+
if err = downloader.WriteTo(reader, output, targetFile); err == nil {
53+
err = decompressData(filepath.Join(output, targetFile))
54+
}
55+
return
56+
}
57+
58+
func SwaggersHandler(w http.ResponseWriter, _ *http.Request,
59+
_ map[string]string) {
60+
swaggers := GetSwaggerList()
61+
if data, err := json.Marshal(swaggers); err == nil {
62+
_, _ = w.Write(data)
63+
} else {
64+
w.WriteHeader(http.StatusInternalServerError)
65+
}
66+
}
67+
68+
func GetSwaggerList() (swaggers []string) {
69+
dataDir := home.GetUserDataDir()
70+
_ = filepath.WalkDir(dataDir, func(path string, d os.DirEntry, err error) error {
71+
if err != nil {
72+
return err
73+
}
74+
75+
if !d.IsDir() && filepath.Ext(path) == ".json" {
76+
swaggers = append(swaggers, filepath.Base(path))
77+
}
78+
return nil
79+
})
80+
return
81+
}
82+
83+
func decompressData(dataFile string) (err error) {
84+
var file *os.File
85+
file, err = os.Open(dataFile)
86+
if err != nil {
87+
return
88+
}
89+
defer file.Close()
90+
91+
var gzipReader *gzip.Reader
92+
gzipReader, err = gzip.NewReader(file)
93+
if err != nil {
94+
return
95+
}
96+
defer gzipReader.Close()
97+
98+
tarReader := tar.NewReader(gzipReader)
99+
100+
for {
101+
header, err := tarReader.Next()
102+
if err == io.EOF {
103+
break // 退出循环
104+
}
105+
if err != nil {
106+
panic(err)
107+
}
108+
109+
destPath := filepath.Join(filepath.Dir(dataFile), filepath.Base(header.Name))
110+
111+
switch header.Typeflag {
112+
case tar.TypeReg:
113+
destFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY, os.FileMode(header.Mode))
114+
if err != nil {
115+
panic(err)
116+
}
117+
defer destFile.Close()
118+
119+
if _, err := io.Copy(destFile, tarReader); err != nil {
120+
panic(err)
121+
}
122+
default:
123+
fmt.Printf("Skipping entry type %c: %s\n", header.Typeflag, header.Name)
124+
}
125+
}
126+
return
127+
}

0 commit comments

Comments
 (0)