diff --git a/Makefile b/Makefile index 43bac5c8..f802d864 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,8 @@ build: go build ${TOOLEXEC} -a -o bin/atest main.go build-ext-git: 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: cd console/atest-ui && npm i && npm run build-only cp console/atest-ui/dist/index.html cmd/data/index.html diff --git a/cmd/server.go b/cmd/server.go index 96912992..5b076b8a 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -119,12 +119,14 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) { 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) if storeKindsErr != nil { cmd.PrintErrf("failed to get store kinds, error: %p\n", storeKindsErr) } else { - if err = startPlugins(o.execer, kinds); err != nil { + if err = startPlugins(storeExtMgr, kinds); err != nil { return } } @@ -146,11 +148,7 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) { <-clean _ = lis.Close() _ = o.httpServer.Shutdown(ctx) - for _, file := range filesNeedToBeRemoved { - if err = os.RemoveAll(file); err != nil { - log.Printf("failed to remove %s, error: %v", file, err) - } - } + _ = storeExtMgr.StopAll() }() 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://" for _, kind := range kinds.Data { if kind.Enabled && strings.HasPrefix(kind.Url, socketPrefix) { - binaryPath, lookErr := execer.LookPath(kind.Name) - if lookErr != nil { - 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) + if err = storeExtMgr.Start(kind.Name, kind.Url); err != nil { + break } } } diff --git a/cmd/server_test.go b/cmd/server_test.go index 813b678b..66707bea 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -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 import ( diff --git a/console/atest-ui/src/views/StoreManager.vue b/console/atest-ui/src/views/StoreManager.vue index d9cbe676..1ac0fd38 100644 --- a/console/atest-ui/src/views/StoreManager.vue +++ b/console/atest-ui/src/views/StoreManager.vue @@ -4,6 +4,7 @@ import { reactive, ref } from 'vue' import { Edit, Delete } from '@element-plus/icons-vue' import type { FormInstance, FormRules } from 'element-plus' import type { Pair } from './types' +import { SupportedExtensions } from './store' import { useI18n } from 'vue-i18n' const { t } = useI18n() @@ -266,7 +267,19 @@ function updateKeys() { - + + + diff --git a/console/atest-ui/src/views/store.ts b/console/atest-ui/src/views/store.ts new file mode 100644 index 00000000..81d36687 --- /dev/null +++ b/console/atest-ui/src/views/store.ts @@ -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[] +} \ No newline at end of file diff --git a/pkg/server/remote_server.go b/pkg/server/remote_server.go index b7ba972c..6781d125 100644 --- a/pkg/server/remote_server.go +++ b/pkg/server/remote_server.go @@ -57,6 +57,7 @@ type server struct { loader testing.Writer storeWriterFactory testing.StoreWriterFactory configDir string + storeExtMgr ExtManager secretServer SecretServiceServer } @@ -97,7 +98,7 @@ func (f *fakeSecretServer) UpdateSecret(ctx context.Context, in *Secret) (reply } // 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 { secretServer = &fakeSecretServer{} } @@ -107,6 +108,7 @@ func NewRemoteServer(loader testing.Writer, storeWriterFactory testing.StoreWrit storeWriterFactory: storeWriterFactory, configDir: configDir, 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) { reply = &Store{} 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 } func (s *server) UpdateStore(ctx context.Context, in *Store) (reply *Store, err error) { reply = &Store{} 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 } func (s *server) DeleteStore(ctx context.Context, in *Store) (reply *Store, err error) { diff --git a/pkg/server/remote_server_test.go b/pkg/server/remote_server_test.go index e68bc137..ef001a4b 100644 --- a/pkg/server/remote_server_test.go +++ b/pkg/server/remote_server_test.go @@ -54,7 +54,7 @@ func TestRemoteServer(t *testing.T) { loader := atesting.NewFileWriter("") loader.Put("testdata/simple.yaml") - server := NewRemoteServer(loader, nil, nil, "") + server := NewRemoteServer(loader, nil, nil, nil, "") _, err := server.Run(ctx, &TestTask{ Kind: "fake", }) @@ -138,7 +138,7 @@ func TestRemoteServer(t *testing.T) { func TestRunTestCase(t *testing.T) { loader := atesting.NewFileWriter("") loader.Put("testdata/simple.yaml") - server := NewRemoteServer(loader, nil, nil, "") + server := NewRemoteServer(loader, nil, nil, nil, "") defer gock.Clean() gock.New(urlFoo).Get("/").MatchHeader("key", "value"). @@ -313,7 +313,7 @@ func TestUpdateTestCase(t *testing.T) { assert.NoError(t, err) ctx := context.Background() - server := NewRemoteServer(writer, nil, nil, "") + server := NewRemoteServer(writer, nil, nil, nil, "") _, err = server.UpdateTestCase(ctx, &TestCaseWithSuite{ SuiteName: "simple", Data: &TestCase{ @@ -385,7 +385,7 @@ func TestListTestCase(t *testing.T) { writer := atesting.NewFileWriter(os.TempDir()) writer.Put(tmpFile.Name()) - server := NewRemoteServer(writer, nil, nil, "") + server := NewRemoteServer(writer, nil, nil, nil, "") ctx := context.Background() t.Run("get two testcases", func(t *testing.T) { @@ -813,7 +813,7 @@ func getRemoteServerInTempDir() (server RunnerServer, call func()) { call = func() { os.RemoveAll(dir) } writer := atesting.NewFileWriter(dir) - server = NewRemoteServer(writer, newLocalloaderFromStore(), nil, dir) + server = NewRemoteServer(writer, newLocalloaderFromStore(), nil, nil, dir) return } diff --git a/pkg/server/store_ext_manager.go b/pkg/server/store_ext_manager.go new file mode 100644 index 00000000..3370c514 --- /dev/null +++ b/pkg/server/store_ext_manager.go @@ -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 +} diff --git a/pkg/server/store_ext_manager_test.go b/pkg/server/store_ext_manager_test.go new file mode 100644 index 00000000..a9d40cec --- /dev/null +++ b/pkg/server/store_ext_manager_test.go @@ -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() + }) +}