feat: support starting store extentsion automatically (#246)

This commit is contained in:
Rick 2023-10-23 15:59:22 +08:00 committed by GitHub
parent 82e91207e9
commit fac641fef9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 219 additions and 28 deletions

View File

@ -11,6 +11,8 @@ build:
go build ${TOOLEXEC} -a -o bin/atest main.go go build ${TOOLEXEC} -a -o bin/atest main.go
build-ext-git: build-ext-git:
CGO_ENABLED=0 go build -ldflags "-w -s" -o bin/atest-store-git extensions/store-git/main.go CGO_ENABLED=0 go build -ldflags "-w -s" -o bin/atest-store-git extensions/store-git/main.go
build-ext-orm:
CGO_ENABLED=0 go build -ldflags "-w -s" -o bin/atest-store-orm extensions/store-orm/main.go
embed-ui: embed-ui:
cd console/atest-ui && npm i && npm run build-only cd console/atest-ui && npm i && npm run build-only
cp console/atest-ui/dist/index.html cmd/data/index.html cp console/atest-ui/dist/index.html cmd/data/index.html

View File

@ -119,12 +119,14 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) {
template.SetSecretGetter(remote.NewGRPCSecretGetter(secretServer)) template.SetSecretGetter(remote.NewGRPCSecretGetter(secretServer))
} }
remoteServer := server.NewRemoteServer(loader, remote.NewGRPCloaderFromStore(), secretServer, o.configDir) storeExtMgr := server.NewStoreExtManager(o.execer)
remoteServer := server.NewRemoteServer(loader, remote.NewGRPCloaderFromStore(), secretServer, storeExtMgr, o.configDir)
kinds, storeKindsErr := remoteServer.GetStoreKinds(ctx, nil) kinds, storeKindsErr := remoteServer.GetStoreKinds(ctx, nil)
if storeKindsErr != nil { if storeKindsErr != nil {
cmd.PrintErrf("failed to get store kinds, error: %p\n", storeKindsErr) cmd.PrintErrf("failed to get store kinds, error: %p\n", storeKindsErr)
} else { } else {
if err = startPlugins(o.execer, kinds); err != nil { if err = startPlugins(storeExtMgr, kinds); err != nil {
return return
} }
} }
@ -146,11 +148,7 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) {
<-clean <-clean
_ = lis.Close() _ = lis.Close()
_ = o.httpServer.Shutdown(ctx) _ = o.httpServer.Shutdown(ctx)
for _, file := range filesNeedToBeRemoved { _ = storeExtMgr.StopAll()
if err = os.RemoveAll(file); err != nil {
log.Printf("failed to remove %s, error: %v", file, err)
}
}
}() }()
mux := runtime.NewServeMux(runtime.WithMetadata(server.MetadataStoreFunc)) // runtime.WithIncomingHeaderMatcher(func(key string) (s string, b bool) { mux := runtime.NewServeMux(runtime.WithMetadata(server.MetadataStoreFunc)) // runtime.WithIncomingHeaderMatcher(func(key string) (s string, b bool) {
@ -200,22 +198,13 @@ func postRequestProxy(proxy string) func(w http.ResponseWriter, r *http.Request,
} }
} }
func startPlugins(execer fakeruntime.Execer, kinds *server.StoreKinds) (err error) { func startPlugins(storeExtMgr server.ExtManager, kinds *server.StoreKinds) (err error) {
const socketPrefix = "unix://" const socketPrefix = "unix://"
for _, kind := range kinds.Data { for _, kind := range kinds.Data {
if kind.Enabled && strings.HasPrefix(kind.Url, socketPrefix) { if kind.Enabled && strings.HasPrefix(kind.Url, socketPrefix) {
binaryPath, lookErr := execer.LookPath(kind.Name) if err = storeExtMgr.Start(kind.Name, kind.Url); err != nil {
if lookErr != nil { break
log.Printf("failed to find %s, error: %v", kind.Name, lookErr)
} else {
go func(socketURL, plugin string) {
socketFile := strings.TrimPrefix(socketURL, socketPrefix)
filesNeedToBeRemoved = append(filesNeedToBeRemoved, socketFile)
if err = execer.RunCommand(plugin, "--socket", socketFile); err != nil {
log.Printf("failed to start %s, error: %v", socketURL, err)
}
}(kind.Url, binaryPath)
} }
} }
} }

View File

@ -1,3 +1,27 @@
/**
MIT License
Copyright (c) 2023 API Testing Authors.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package cmd package cmd
import ( import (

View File

@ -4,6 +4,7 @@ import { reactive, ref } from 'vue'
import { Edit, Delete } from '@element-plus/icons-vue' import { Edit, Delete } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import type { Pair } from './types' import type { Pair } from './types'
import { SupportedExtensions } from './store'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
const { t } = useI18n() const { t } = useI18n()
@ -266,7 +267,19 @@ function updateKeys() {
<el-input v-model="storeForm.password" type="password" test-id="store-form-password" /> <el-input v-model="storeForm.password" type="password" test-id="store-form-password" />
</el-form-item> </el-form-item>
<el-form-item :label="t('field.pluginName')" prop="pluginName"> <el-form-item :label="t('field.pluginName')" prop="pluginName">
<el-input v-model="storeForm.kind.name" test-id="store-form-plugin-name" /> <el-select
v-model="storeForm.kind.name"
test-id="store-form-plugin-name"
class="m-2"
size="middle"
>
<el-option
v-for="item in SupportedExtensions()"
:key="item.value"
:label="item.key"
:value="item.value"
/>
</el-select>
</el-form-item> </el-form-item>
<el-form-item :label="t('field.pluginURL')" prop="plugin"> <el-form-item :label="t('field.pluginURL')" prop="plugin">
<el-input v-model="storeForm.kind.url" test-id="store-form-plugin" /> <el-input v-model="storeForm.kind.url" test-id="store-form-plugin" />

View File

@ -0,0 +1,18 @@
import { Pair } from './types'
export function SupportedExtensions() {
return [
{
value: 'atest-store-git',
key: 'atest-store-git'
},
{
value: 'atest-store-s3',
key: 'atest-store-s3'
},
{
value: 'atest-store-orm',
key: 'atest-store-orm'
}
] as Pair[]
}

View File

@ -57,6 +57,7 @@ type server struct {
loader testing.Writer loader testing.Writer
storeWriterFactory testing.StoreWriterFactory storeWriterFactory testing.StoreWriterFactory
configDir string configDir string
storeExtMgr ExtManager
secretServer SecretServiceServer secretServer SecretServiceServer
} }
@ -97,7 +98,7 @@ func (f *fakeSecretServer) UpdateSecret(ctx context.Context, in *Secret) (reply
} }
// NewRemoteServer creates a remote server instance // NewRemoteServer creates a remote server instance
func NewRemoteServer(loader testing.Writer, storeWriterFactory testing.StoreWriterFactory, secretServer SecretServiceServer, configDir string) RunnerServer { func NewRemoteServer(loader testing.Writer, storeWriterFactory testing.StoreWriterFactory, secretServer SecretServiceServer, storeExtMgr ExtManager, configDir string) RunnerServer {
if secretServer == nil { if secretServer == nil {
secretServer = &fakeSecretServer{} secretServer = &fakeSecretServer{}
} }
@ -107,6 +108,7 @@ func NewRemoteServer(loader testing.Writer, storeWriterFactory testing.StoreWrit
storeWriterFactory: storeWriterFactory, storeWriterFactory: storeWriterFactory,
configDir: configDir, configDir: configDir,
secretServer: secretServer, secretServer: secretServer,
storeExtMgr: storeExtMgr,
} }
} }
@ -829,13 +831,19 @@ func (s *server) GetStores(ctx context.Context, in *Empty) (reply *Stores, err e
func (s *server) CreateStore(ctx context.Context, in *Store) (reply *Store, err error) { func (s *server) CreateStore(ctx context.Context, in *Store) (reply *Store, err error) {
reply = &Store{} reply = &Store{}
storeFactory := testing.NewStoreFactory(s.configDir) storeFactory := testing.NewStoreFactory(s.configDir)
err = storeFactory.CreateStore(ToNormalStore(in)) store := ToNormalStore(in)
if err = storeFactory.CreateStore(store); err == nil && s.storeExtMgr != nil {
err = s.storeExtMgr.Start(store.Kind.Name, store.Kind.URL)
}
return return
} }
func (s *server) UpdateStore(ctx context.Context, in *Store) (reply *Store, err error) { func (s *server) UpdateStore(ctx context.Context, in *Store) (reply *Store, err error) {
reply = &Store{} reply = &Store{}
storeFactory := testing.NewStoreFactory(s.configDir) storeFactory := testing.NewStoreFactory(s.configDir)
err = storeFactory.UpdateStore(ToNormalStore(in)) store := ToNormalStore(in)
if err = storeFactory.UpdateStore(store); err == nil && s.storeExtMgr != nil {
err = s.storeExtMgr.Start(store.Kind.Name, store.Kind.URL)
}
return return
} }
func (s *server) DeleteStore(ctx context.Context, in *Store) (reply *Store, err error) { func (s *server) DeleteStore(ctx context.Context, in *Store) (reply *Store, err error) {

View File

@ -54,7 +54,7 @@ func TestRemoteServer(t *testing.T) {
loader := atesting.NewFileWriter("") loader := atesting.NewFileWriter("")
loader.Put("testdata/simple.yaml") loader.Put("testdata/simple.yaml")
server := NewRemoteServer(loader, nil, nil, "") server := NewRemoteServer(loader, nil, nil, nil, "")
_, err := server.Run(ctx, &TestTask{ _, err := server.Run(ctx, &TestTask{
Kind: "fake", Kind: "fake",
}) })
@ -138,7 +138,7 @@ func TestRemoteServer(t *testing.T) {
func TestRunTestCase(t *testing.T) { func TestRunTestCase(t *testing.T) {
loader := atesting.NewFileWriter("") loader := atesting.NewFileWriter("")
loader.Put("testdata/simple.yaml") loader.Put("testdata/simple.yaml")
server := NewRemoteServer(loader, nil, nil, "") server := NewRemoteServer(loader, nil, nil, nil, "")
defer gock.Clean() defer gock.Clean()
gock.New(urlFoo).Get("/").MatchHeader("key", "value"). gock.New(urlFoo).Get("/").MatchHeader("key", "value").
@ -313,7 +313,7 @@ func TestUpdateTestCase(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
ctx := context.Background() ctx := context.Background()
server := NewRemoteServer(writer, nil, nil, "") server := NewRemoteServer(writer, nil, nil, nil, "")
_, err = server.UpdateTestCase(ctx, &TestCaseWithSuite{ _, err = server.UpdateTestCase(ctx, &TestCaseWithSuite{
SuiteName: "simple", SuiteName: "simple",
Data: &TestCase{ Data: &TestCase{
@ -385,7 +385,7 @@ func TestListTestCase(t *testing.T) {
writer := atesting.NewFileWriter(os.TempDir()) writer := atesting.NewFileWriter(os.TempDir())
writer.Put(tmpFile.Name()) writer.Put(tmpFile.Name())
server := NewRemoteServer(writer, nil, nil, "") server := NewRemoteServer(writer, nil, nil, nil, "")
ctx := context.Background() ctx := context.Background()
t.Run("get two testcases", func(t *testing.T) { t.Run("get two testcases", func(t *testing.T) {
@ -813,7 +813,7 @@ func getRemoteServerInTempDir() (server RunnerServer, call func()) {
call = func() { os.RemoveAll(dir) } call = func() { os.RemoveAll(dir) }
writer := atesting.NewFileWriter(dir) writer := atesting.NewFileWriter(dir)
server = NewRemoteServer(writer, newLocalloaderFromStore(), nil, dir) server = NewRemoteServer(writer, newLocalloaderFromStore(), nil, nil, dir)
return return
} }

View File

@ -0,0 +1,89 @@
/**
MIT License
Copyright (c) 2023 API Testing Authors.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package server
import (
"fmt"
"log"
"os"
"strings"
fakeruntime "github.com/linuxsuren/go-fake-runtime"
)
type ExtManager interface {
Start(name, socket string) (err error)
StopAll() (err error)
}
type storeExtManager struct {
stopSignal chan struct{}
execer fakeruntime.Execer
socketPrefix string
filesNeedToBeRemoved []string
extStatusMap map[string]bool
}
var s *storeExtManager
func NewStoreExtManager(execer fakeruntime.Execer) ExtManager {
if s == nil {
s = &storeExtManager{}
s.execer = execer
s.socketPrefix = "unix://"
s.extStatusMap = map[string]bool{}
}
return s
}
func (s *storeExtManager) Start(name, socket string) (err error) {
if v, ok := s.extStatusMap[name]; ok && v {
return
}
binaryPath, lookErr := s.execer.LookPath(name)
if lookErr != nil {
err = fmt.Errorf("failed to find %s, error: %v", name, lookErr)
} else {
go func(socketURL, plugin string) {
socketFile := strings.TrimPrefix(socketURL, s.socketPrefix)
s.filesNeedToBeRemoved = append(s.filesNeedToBeRemoved, socketFile)
s.extStatusMap[name] = true
if err = s.execer.RunCommand(plugin, "--socket", socketFile); err != nil {
log.Printf("failed to start %s, error: %v", socketURL, err)
}
}(socket, binaryPath)
}
return
}
func (s *storeExtManager) StopAll() error {
for _, file := range s.filesNeedToBeRemoved {
if err := os.RemoveAll(file); err != nil {
log.Printf("failed to remove %s, error: %v", file, err)
}
}
return nil
}

View File

@ -0,0 +1,48 @@
/**
MIT License
Copyright (c) 2023 API Testing Authors.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package server
import (
"testing"
fakeruntime "github.com/linuxsuren/go-fake-runtime"
"github.com/stretchr/testify/assert"
)
func TestStoreExtManager(t *testing.T) {
mgr := NewStoreExtManager(fakeruntime.DefaultExecer{})
t.Run("not found", func(t *testing.T) {
err := mgr.Start("fake", "")
assert.Error(t, err)
})
t.Run("exist executable file", func(t *testing.T) {
err := mgr.Start("go", "")
assert.NoError(t, err)
err = mgr.StopAll()
})
}