mirror of
https://github.com/gusaul/grpcox.git
synced 2024-12-26 02:40:10 +00:00
feat(descriptor): add support for local proto descriptor
Currently, grpcox depends on server reflection to get proto descriptor. It has a significant drawback, since not every grpc server support [server reflection](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md#known-implementations). Using local proto files is more feasible, as every grpc server certainly have one. Even though using protofile should be simple enough, there's still a problem regarding this. Some protofile use extra plugins for their proto. i.e. gogoprotobuf is a project that does just that. The problems with plugins are most of them require explicit import to the plugin inside of the protofile. It will break grpcurl proto descriptor extraction. Thus, the plugin proto must be uploaded alongside the protofile. Also, the protofile should be modified automatically to change their import to local import. Given that, I proposed a way for the user to upload multiple protofile to grpcox. Then, use that to get the descriptor. Changelog: - Add `use local proto` checkbox in HTML client. On checked it will show upload button and list of selected proto. - `get-service` ajax will use POST when `use local proto` is checked. The uploaded protofile will be the payload for the ajax request. - Add a new route to handle POST "get-service". It will persist the uploaded protofile to `/tmp/` directory and add protos field in the resource. - Modify `openDescriptor` to use local proto if protos field in the resource is available. - Modify `openDescriptor` to return an error, as opening descriptor from local proto may fail. - Modify the main server so it can be shut down gracefully. This is necessary as grpcox need to remove persisted proto right after the server is turned off. This Pull Request will resolve #16
This commit is contained in:
parent
3633418ad0
commit
293bb364c0
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
||||||
grpcox
|
grpcox
|
||||||
log
|
log
|
||||||
|
*.out
|
|
@ -3,6 +3,7 @@ package core
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -31,6 +32,14 @@ type GrpCox struct {
|
||||||
isUnixSocket func() bool
|
isUnixSocket func() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Proto define protofile uploaded from client
|
||||||
|
// will be used to be persisted to disk and indicator
|
||||||
|
// whether connections should reflect from server or local proto
|
||||||
|
type Proto struct {
|
||||||
|
Name string
|
||||||
|
Content []byte
|
||||||
|
}
|
||||||
|
|
||||||
// InitGrpCox constructor
|
// InitGrpCox constructor
|
||||||
func InitGrpCox() *GrpCox {
|
func InitGrpCox() *GrpCox {
|
||||||
maxLife, tick := 10, 3
|
maxLife, tick := 10, 3
|
||||||
|
@ -80,6 +89,24 @@ func (g *GrpCox) GetResource(ctx context.Context, target string, plainText, isRe
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetResourceWithProto - open resource to targeted grpc server using given protofile
|
||||||
|
func (g *GrpCox) GetResourceWithProto(ctx context.Context, target string, plainText, isRestartConn bool, protos []Proto) (*Resource, error) {
|
||||||
|
r, err := g.GetResource(ctx, target, plainText, isRestartConn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if given protofile is equal to current, skip adding protos as it's already
|
||||||
|
// persisted in the harddisk anyway
|
||||||
|
if reflect.DeepEqual(r.protos, protos) {
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// add protos property to resource and persist it to harddisk
|
||||||
|
err = r.AddProtos(protos)
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
|
||||||
// GetActiveConns - get all saved active connection
|
// GetActiveConns - get all saved active connection
|
||||||
func (g *GrpCox) GetActiveConns(ctx context.Context) []string {
|
func (g *GrpCox) GetActiveConns(ctx context.Context) []string {
|
||||||
active := g.activeConn.getAllConn()
|
active := g.activeConn.getAllConn()
|
||||||
|
|
121
core/resource.go
121
core/resource.go
|
@ -5,8 +5,13 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fullstorydev/grpcurl"
|
"github.com/fullstorydev/grpcurl"
|
||||||
|
@ -18,22 +23,43 @@ import (
|
||||||
reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
|
reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// BasePath define path where proto file will persisted
|
||||||
|
const BasePath = "/tmp/grpcox/"
|
||||||
|
|
||||||
// Resource - hold 3 main function (List, Describe, and Invoke)
|
// Resource - hold 3 main function (List, Describe, and Invoke)
|
||||||
type Resource struct {
|
type Resource struct {
|
||||||
clientConn *grpc.ClientConn
|
clientConn *grpc.ClientConn
|
||||||
descSource grpcurl.DescriptorSource
|
descSource grpcurl.DescriptorSource
|
||||||
refClient *grpcreflect.Client
|
refClient *grpcreflect.Client
|
||||||
|
protos []Proto
|
||||||
|
|
||||||
headers []string
|
headers []string
|
||||||
md metadata.MD
|
md metadata.MD
|
||||||
}
|
}
|
||||||
|
|
||||||
//openDescriptor - use it to reflect server descriptor
|
//openDescriptor - use it to reflect server descriptor
|
||||||
func (r *Resource) openDescriptor() {
|
func (r *Resource) openDescriptor() error {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
refCtx := metadata.NewOutgoingContext(ctx, r.md)
|
refCtx := metadata.NewOutgoingContext(ctx, r.md)
|
||||||
r.refClient = grpcreflect.NewClient(refCtx, reflectpb.NewServerReflectionClient(r.clientConn))
|
r.refClient = grpcreflect.NewClient(refCtx, reflectpb.NewServerReflectionClient(r.clientConn))
|
||||||
|
|
||||||
|
// if no protos available use server reflection
|
||||||
|
if r.protos == nil {
|
||||||
r.descSource = grpcurl.DescriptorSourceFromServer(ctx, r.refClient)
|
r.descSource = grpcurl.DescriptorSourceFromServer(ctx, r.refClient)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
protoPath := filepath.Join(BasePath, r.clientConn.Target())
|
||||||
|
|
||||||
|
// make list of protos name to be used as descriptor
|
||||||
|
protos := make([]string, 0, len(r.protos))
|
||||||
|
for _, proto := range r.protos {
|
||||||
|
protos = append(protos, proto.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
r.descSource, err = grpcurl.DescriptorSourceFromProtoFiles([]string{protoPath}, protos...)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
//closeDescriptor - please ensure to always close after open in the same flow
|
//closeDescriptor - please ensure to always close after open in the same flow
|
||||||
|
@ -50,7 +76,7 @@ func (r *Resource) closeDescriptor() {
|
||||||
case <-done:
|
case <-done:
|
||||||
return
|
return
|
||||||
case <-time.After(3 * time.Second):
|
case <-time.After(3 * time.Second):
|
||||||
log.Printf("Reflection %s falied to close\n", r.clientConn.Target())
|
log.Printf("Reflection %s failed to close\n", r.clientConn.Target())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,7 +85,10 @@ func (r *Resource) closeDescriptor() {
|
||||||
// symbol can be "" to list all available services
|
// symbol can be "" to list all available services
|
||||||
// symbol also can be service name to list all available method
|
// symbol also can be service name to list all available method
|
||||||
func (r *Resource) List(symbol string) ([]string, error) {
|
func (r *Resource) List(symbol string) ([]string, error) {
|
||||||
r.openDescriptor()
|
err := r.openDescriptor()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
defer r.closeDescriptor()
|
defer r.closeDescriptor()
|
||||||
|
|
||||||
var result []string
|
var result []string
|
||||||
|
@ -97,7 +126,10 @@ func (r *Resource) List(symbol string) ([]string, error) {
|
||||||
// It also prints a description of that symbol, in the form of snippets of proto source.
|
// It also prints a description of that symbol, in the form of snippets of proto source.
|
||||||
// It won't necessarily be the original source that defined the element, but it will be equivalent.
|
// It won't necessarily be the original source that defined the element, but it will be equivalent.
|
||||||
func (r *Resource) Describe(symbol string) (string, string, error) {
|
func (r *Resource) Describe(symbol string) (string, string, error) {
|
||||||
r.openDescriptor()
|
err := r.openDescriptor()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
defer r.closeDescriptor()
|
defer r.closeDescriptor()
|
||||||
|
|
||||||
var result, template string
|
var result, template string
|
||||||
|
@ -153,7 +185,10 @@ func (r *Resource) Describe(symbol string) (string, string, error) {
|
||||||
|
|
||||||
// Invoke - invoking gRPC function
|
// Invoke - invoking gRPC function
|
||||||
func (r *Resource) Invoke(ctx context.Context, symbol string, in io.Reader) (string, time.Duration, error) {
|
func (r *Resource) Invoke(ctx context.Context, symbol string, in io.Reader) (string, time.Duration, error) {
|
||||||
r.openDescriptor()
|
err := r.openDescriptor()
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
defer r.closeDescriptor()
|
defer r.closeDescriptor()
|
||||||
|
|
||||||
// because of grpcurl directly fmt.Printf on their invoke function
|
// because of grpcurl directly fmt.Printf on their invoke function
|
||||||
|
@ -202,20 +237,37 @@ func (r *Resource) Invoke(ctx context.Context, symbol string, in io.Reader) (str
|
||||||
|
|
||||||
// Close - to close all resources that was opened before
|
// Close - to close all resources that was opened before
|
||||||
func (r *Resource) Close() {
|
func (r *Resource) Close() {
|
||||||
done := make(chan int)
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
if r.clientConn != nil {
|
if r.clientConn != nil {
|
||||||
r.clientConn.Close()
|
r.clientConn.Close()
|
||||||
r.clientConn = nil
|
r.clientConn = nil
|
||||||
}
|
}
|
||||||
done <- 1
|
}()
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
err := os.RemoveAll(BasePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error removing proto dir from tmp: %s", err.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
c := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(c)
|
||||||
|
wg.Wait()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-done:
|
case <-c:
|
||||||
return
|
return
|
||||||
case <-time.After(3 * time.Second):
|
case <-time.After(3 * time.Second):
|
||||||
log.Printf("Connection %s falied to close\n", r.clientConn.Target())
|
log.Printf("Connection %s failed to close\n", r.clientConn.Target())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -229,3 +281,54 @@ func (r *Resource) exit(code int) {
|
||||||
r.Close()
|
r.Close()
|
||||||
os.Exit(code)
|
os.Exit(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddProtos to resource properties and harddisk
|
||||||
|
// added protos will be persisted in `basepath + connection target`
|
||||||
|
// i.e. connection target == 127.0.0.1:8888
|
||||||
|
// proto files will be persisted in /tmp/grpcox/127.0.0.1:8888
|
||||||
|
// if the directory is already there, remove it first
|
||||||
|
func (r *Resource) AddProtos(protos []Proto) error {
|
||||||
|
protoPath := filepath.Join(BasePath, r.clientConn.Target())
|
||||||
|
err := os.MkdirAll(protoPath, 0777)
|
||||||
|
if os.IsExist(err) {
|
||||||
|
os.RemoveAll(protoPath)
|
||||||
|
err = os.MkdirAll(protoPath, 0777)
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, proto := range protos {
|
||||||
|
err := ioutil.WriteFile(filepath.Join(protoPath, "/", proto.Name),
|
||||||
|
prepareImport(proto.Content),
|
||||||
|
0777)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.protos = protos
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareImport transforming proto import into local path
|
||||||
|
// with exception to google proto import as it won't cause any problem
|
||||||
|
func prepareImport(proto []byte) []byte {
|
||||||
|
const pattern = `import ".+`
|
||||||
|
result := string(proto)
|
||||||
|
|
||||||
|
re := regexp.MustCompile(pattern)
|
||||||
|
matchs := re.FindAllString(result, -1)
|
||||||
|
for _, match := range matchs {
|
||||||
|
if strings.Contains(match, "\"google/") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := strings.Split(match, "/")
|
||||||
|
if len(name) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
importString := `import "` + name[len(name)-1]
|
||||||
|
result = strings.Replace(result, match, importString, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(result)
|
||||||
|
}
|
||||||
|
|
70
core/resource_test.go
Normal file
70
core/resource_test.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_prepareImport(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
proto []byte
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want []byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "sucess change import path to local",
|
||||||
|
args: args{
|
||||||
|
proto: []byte(`
|
||||||
|
package testing;
|
||||||
|
|
||||||
|
import "test.com/owner/repo/content.proto";`),
|
||||||
|
},
|
||||||
|
want: []byte(`
|
||||||
|
package testing;
|
||||||
|
|
||||||
|
import "content.proto";`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sucess keep google import",
|
||||||
|
args: args{
|
||||||
|
proto: []byte(`
|
||||||
|
package testing;
|
||||||
|
|
||||||
|
import "google/proto/buf";
|
||||||
|
import "test.com/owner/repo/content.proto";`),
|
||||||
|
},
|
||||||
|
want: []byte(`
|
||||||
|
package testing;
|
||||||
|
|
||||||
|
import "google/proto/buf";
|
||||||
|
import "content.proto";`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sucess keep local import",
|
||||||
|
args: args{
|
||||||
|
proto: []byte(`
|
||||||
|
package testing;
|
||||||
|
|
||||||
|
import "repo.proto";
|
||||||
|
import "test.com/owner/repo/content.proto";`),
|
||||||
|
},
|
||||||
|
want: []byte(`
|
||||||
|
package testing;
|
||||||
|
|
||||||
|
import "repo.proto";
|
||||||
|
import "content.proto";`),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := prepareImport(tt.args.proto); !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("prepareImport() = %v, want %v",
|
||||||
|
string(got),
|
||||||
|
string(tt.want))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
44
grpcox.go
44
grpcox.go
|
@ -1,13 +1,16 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/gusaul/grpcox/core"
|
||||||
"github.com/gusaul/grpcox/handler"
|
"github.com/gusaul/grpcox/handler"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -25,13 +28,48 @@ func main() {
|
||||||
port := ":6969"
|
port := ":6969"
|
||||||
muxRouter := mux.NewRouter()
|
muxRouter := mux.NewRouter()
|
||||||
handler.Init(muxRouter)
|
handler.Init(muxRouter)
|
||||||
|
var wait time.Duration = time.Second * 15
|
||||||
|
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Handler: muxRouter,
|
|
||||||
Addr: "0.0.0.0" + port,
|
Addr: "0.0.0.0" + port,
|
||||||
WriteTimeout: 15 * time.Second,
|
WriteTimeout: time.Second * 15,
|
||||||
ReadTimeout: 15 * time.Second,
|
ReadTimeout: time.Second * 15,
|
||||||
|
IdleTimeout: time.Second * 60,
|
||||||
|
Handler: muxRouter,
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Service started on", port)
|
fmt.Println("Service started on", port)
|
||||||
|
go func() {
|
||||||
log.Fatal(srv.ListenAndServe())
|
log.Fatal(srv.ListenAndServe())
|
||||||
|
}()
|
||||||
|
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
|
||||||
|
// We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
|
||||||
|
// SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught.
|
||||||
|
signal.Notify(c, os.Interrupt)
|
||||||
|
|
||||||
|
// Block until we receive our signal.
|
||||||
|
<-c
|
||||||
|
|
||||||
|
// Create a deadline to wait for.
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), wait)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err = removeProtos()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error while removing protos: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.Shutdown(ctx)
|
||||||
|
log.Println("shutting down")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeProtos will remove all uploaded proto file
|
||||||
|
// this function will be called as the server shutdown gracefully
|
||||||
|
func removeProtos() error {
|
||||||
|
log.Println("removing proto dir from /tmp")
|
||||||
|
err := os.RemoveAll(core.BasePath)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -85,6 +86,65 @@ func (h *Handler) getLists(w http.ResponseWriter, r *http.Request) {
|
||||||
response(w, result)
|
response(w, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getListsWithProto handling client request for service list with proto
|
||||||
|
func (h *Handler) getListsWithProto(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
host := vars["host"]
|
||||||
|
if host == "" {
|
||||||
|
writeError(w, fmt.Errorf("Invalid Host"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
service := vars["serv_name"]
|
||||||
|
|
||||||
|
useTLS, _ := strconv.ParseBool(r.Header.Get("use_tls"))
|
||||||
|
restart, _ := strconv.ParseBool(r.FormValue("restart"))
|
||||||
|
|
||||||
|
// limit upload file to 5mb
|
||||||
|
err := r.ParseMultipartForm(5 << 20)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert uploaded files to list of Proto struct
|
||||||
|
files := r.MultipartForm.File["protos"]
|
||||||
|
protos := make([]core.Proto, 0, len(files))
|
||||||
|
for _, file := range files {
|
||||||
|
fileData, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer fileData.Close()
|
||||||
|
|
||||||
|
content, err := ioutil.ReadAll(fileData)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
protos = append(protos, core.Proto{
|
||||||
|
Name: file.Filename,
|
||||||
|
Content: content,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := h.g.GetResourceWithProto(context.Background(), host, !useTLS, restart, protos)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := res.List(service)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.g.Extend(host)
|
||||||
|
response(w, result)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) describeFunction(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) describeFunction(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
host := vars["host"]
|
host := vars["host"]
|
||||||
|
|
|
@ -14,6 +14,7 @@ func Init(router *mux.Router) {
|
||||||
|
|
||||||
ajaxRoute := router.PathPrefix("/server/{host}").Subrouter()
|
ajaxRoute := router.PathPrefix("/server/{host}").Subrouter()
|
||||||
ajaxRoute.HandleFunc("/services", corsHandler(h.getLists)).Methods(http.MethodGet, http.MethodOptions)
|
ajaxRoute.HandleFunc("/services", corsHandler(h.getLists)).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
ajaxRoute.HandleFunc("/services", corsHandler(h.getListsWithProto)).Methods(http.MethodPost)
|
||||||
ajaxRoute.HandleFunc("/service/{serv_name}/functions", corsHandler(h.getLists)).Methods(http.MethodGet, http.MethodOptions)
|
ajaxRoute.HandleFunc("/service/{serv_name}/functions", corsHandler(h.getLists)).Methods(http.MethodGet, http.MethodOptions)
|
||||||
ajaxRoute.HandleFunc("/function/{func_name}/describe", corsHandler(h.describeFunction)).Methods(http.MethodGet, http.MethodOptions)
|
ajaxRoute.HandleFunc("/function/{func_name}/describe", corsHandler(h.describeFunction)).Methods(http.MethodGet, http.MethodOptions)
|
||||||
ajaxRoute.HandleFunc("/function/{func_name}/invoke", corsHandler(h.invokeFunction)).Methods(http.MethodPost, http.MethodOptions)
|
ajaxRoute.HandleFunc("/function/{func_name}/invoke", corsHandler(h.invokeFunction)).Methods(http.MethodPost, http.MethodOptions)
|
||||||
|
|
115
index/css/proto.css
Normal file
115
index/css/proto.css
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
#proto-input {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="file"] {
|
||||||
|
border: 0;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute !important;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
[type="file"] + label {
|
||||||
|
background-color: #59698d;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-family: 'Poppins', sans-serif;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 5px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="file"]:focus + label,
|
||||||
|
[type="file"] + label:hover {
|
||||||
|
background-color: #324674;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type="file"]:focus + label {
|
||||||
|
outline: 1px dotted #000;
|
||||||
|
outline: -webkit-focus-ring-color auto 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proto-top-collection {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proto-toggle {
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proto-toggle:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proto-collection {
|
||||||
|
background-color: #e0dfe6;
|
||||||
|
border-radius: 5px;
|
||||||
|
min-height: 120px;
|
||||||
|
width: 100%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
align-content: center;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 20px;
|
||||||
|
padding-right: 0px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proto-item {
|
||||||
|
height: 80px;
|
||||||
|
margin-right: 20px;
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proto-icon {
|
||||||
|
height: 80px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proto-desc {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proto-caption {
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(0,0,0,0.6);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proto-size {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: rgba(0,0,0,0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.proto-remove {
|
||||||
|
background-color: rgba(0,0,0,0.5);
|
||||||
|
padding: 3px !important;
|
||||||
|
margin-left: 0px !important;
|
||||||
|
-webkit-box-shadow: none;
|
||||||
|
-moz-box-shadow: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proto-remove:hover {
|
||||||
|
background-color: rgba(255,0,0,0.4);
|
||||||
|
}
|
BIN
index/img/file.png
Normal file
BIN
index/img/file.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
|
@ -10,6 +10,7 @@
|
||||||
<link href="css/bootstrap.min.css" rel="stylesheet">
|
<link href="css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link href="css/mdb.min.css" rel="stylesheet">
|
<link href="css/mdb.min.css" rel="stylesheet">
|
||||||
<link href="css/style.css" rel="stylesheet">
|
<link href="css/style.css" rel="stylesheet">
|
||||||
|
<link href="css/proto.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@ -28,6 +29,24 @@
|
||||||
<label class="custom-control-label" for="restart-conn">Restart Connection</label>
|
<label class="custom-control-label" for="restart-conn">Restart Connection</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="custom-control custom-checkbox">
|
||||||
|
<input type="checkbox" class="custom-control-input" id="local-proto">
|
||||||
|
<label class="custom-control-label" for="local-proto">Use local proto</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group" id="proto-input" style="display: none">
|
||||||
|
<div class="proto-top-collection">
|
||||||
|
<input class="proto-uploader" type="file" id="proto-file" multiple>
|
||||||
|
<label for="proto-file"><i class="fa fa-plus-circle"></i> proto files</label>
|
||||||
|
|
||||||
|
<span id="proto-collection-toggle" class="proto-toggle">Hide Proto Collection</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="proto-collection"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="other-elem" id="choose-service" style="display: none">
|
<div class="other-elem" id="choose-service" style="display: none">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
|
@ -118,6 +137,7 @@
|
||||||
<script src="https://cdn.rawgit.com/google/code-prettify/master/loader/run_prettify.js"></script>
|
<script src="https://cdn.rawgit.com/google/code-prettify/master/loader/run_prettify.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.1/ace.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.1/ace.js"></script>
|
||||||
<script type="text/javascript" src="js/style.js"></script>
|
<script type="text/javascript" src="js/style.js"></script>
|
||||||
|
<script type="text/javascript" src="js/proto.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
165
index/js/proto.js
Normal file
165
index/js/proto.js
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
let protoMap;
|
||||||
|
let showCollection;
|
||||||
|
|
||||||
|
// IIFE to setup event listener
|
||||||
|
(function () {
|
||||||
|
// add event listener on proto checkbox
|
||||||
|
// if checked the uploader and proto collection view will be displayed
|
||||||
|
// also the grpc reflection will use given proto instead of server reflection
|
||||||
|
const protoSwitch = document.getElementById("local-proto");
|
||||||
|
protoSwitch.addEventListener("change", function(event) {
|
||||||
|
const { checked } = event.target;
|
||||||
|
toggleDisplayProtoInput(checked);
|
||||||
|
});
|
||||||
|
|
||||||
|
// add event listener on upload proto file
|
||||||
|
// uploaded file extension will be checked, only those with *.proto will be processed
|
||||||
|
// add successful file to protoMap to be displayed
|
||||||
|
const protoUploader = document.getElementById("proto-file");
|
||||||
|
protoUploader.addEventListener("change", handleProtoUpload, false);
|
||||||
|
|
||||||
|
// add event listener on toggle proto collection display
|
||||||
|
// it will show / hide the proto collection
|
||||||
|
const protoCollectionToggle = document.getElementById("proto-collection-toggle");
|
||||||
|
protoCollectionToggle.addEventListener("click", toggleDisplayProtoCollection);
|
||||||
|
|
||||||
|
// init map to handle proto files
|
||||||
|
// every proto files will be unique to their name
|
||||||
|
protoMap = new Map();
|
||||||
|
|
||||||
|
// set proto collection display status to true
|
||||||
|
// by default the collection will be shown
|
||||||
|
showCollection = true;
|
||||||
|
}());
|
||||||
|
|
||||||
|
// toggle proto files display
|
||||||
|
// displaying upload button
|
||||||
|
function toggleDisplayProtoInput(show) {
|
||||||
|
const style = show ? "display: block" : "display: none";
|
||||||
|
|
||||||
|
const protoInput = document.getElementById("proto-input");
|
||||||
|
protoInput.removeAttribute("style");
|
||||||
|
protoInput.style.cssText = style;
|
||||||
|
}
|
||||||
|
|
||||||
|
// toggle proto files collection
|
||||||
|
// displaying uploaded protos collection
|
||||||
|
function toggleDisplayProtoCollection() {
|
||||||
|
const protoCollection = document.getElementsByClassName("proto-collection")[0];
|
||||||
|
protoCollection.removeAttribute("style");
|
||||||
|
const protoToggle = document.getElementById("proto-collection-toggle");
|
||||||
|
|
||||||
|
let collectionStyle = "";
|
||||||
|
let toggleText = protoToggle.innerHTML;
|
||||||
|
|
||||||
|
if (showCollection) {
|
||||||
|
collectionStyle = "display: none";
|
||||||
|
toggleText = toggleText.replace("Hide", "Show");
|
||||||
|
} else {
|
||||||
|
collectionStyle = "display: block";
|
||||||
|
toggleText = toggleText.replace("Show", "Hide");
|
||||||
|
}
|
||||||
|
|
||||||
|
protoCollection.style.cssText = collectionStyle;
|
||||||
|
protoToggle.innerHTML = toggleText;
|
||||||
|
showCollection = !showCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handling file upload event
|
||||||
|
// add uploaded files to protoMap to avoid duplication, on file with same name the older
|
||||||
|
// file will be replaced with the latest
|
||||||
|
// if the file isn't available before, add DOM element for UI representation
|
||||||
|
// file without *.proto won't be processed
|
||||||
|
function handleProtoUpload() {
|
||||||
|
const files = this.files;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (!file.name.endsWith(".proto")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!protoMap.has(file.name)) {
|
||||||
|
addProtoItem(file.name, file.size);
|
||||||
|
}
|
||||||
|
protoMap.set(file.name, file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// adding proto item to proto collection view
|
||||||
|
// give visual representation to user and access to remove unwanted uploaded files
|
||||||
|
function addProtoItem(name, size) {
|
||||||
|
const protoItem = createProtoItem(name, size);
|
||||||
|
const collection = document.getElementsByClassName("proto-collection")[0];
|
||||||
|
collection.appendChild(protoItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create dom element for proto item
|
||||||
|
// every item will be given id corresponding to it's name for easier access
|
||||||
|
function createProtoItem(name, size) {
|
||||||
|
const item = document.createElement("div");
|
||||||
|
item.classList.add("proto-item");
|
||||||
|
item.id = name;
|
||||||
|
|
||||||
|
const icon = document.createElement("img");
|
||||||
|
icon.src = "img/file.png";
|
||||||
|
icon.alt = "file icon";
|
||||||
|
icon.classList.add("proto-icon");
|
||||||
|
item.appendChild(icon);
|
||||||
|
|
||||||
|
const desc = createProtoItemDesc(name, size);
|
||||||
|
item.appendChild(desc);
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create dom element for proto item description
|
||||||
|
function createProtoItemDesc(name, size) {
|
||||||
|
const desc = document.createElement("div");
|
||||||
|
desc.classList.add("proto-desc");
|
||||||
|
|
||||||
|
const caption = document.createElement("span");
|
||||||
|
caption.classList.add("proto-caption");
|
||||||
|
const captionText = document.createTextNode(name.length > 15 ?
|
||||||
|
name.substring(0, 12) + "..." :
|
||||||
|
name);
|
||||||
|
caption.appendChild(captionText);
|
||||||
|
desc.appendChild(caption);
|
||||||
|
|
||||||
|
const sizeDOM = document.createElement("span");
|
||||||
|
sizeDOM.classList.add("proto-size");
|
||||||
|
const sizeText = document.createTextNode(size > 1000 ?
|
||||||
|
`${size/1000}kb` :
|
||||||
|
`${size}b`);
|
||||||
|
sizeDOM.appendChild(sizeText);
|
||||||
|
desc.appendChild(sizeDOM);
|
||||||
|
|
||||||
|
const remove = document.createElement("button");
|
||||||
|
remove.classList.add("btn", "btn-sm", "proto-remove");
|
||||||
|
remove.addEventListener("click", function() {removeProtoItem(name);});
|
||||||
|
const removeIcon = document.createElement("i");
|
||||||
|
removeIcon.classList.add("fa", "fa-trash");
|
||||||
|
remove.appendChild(removeIcon);
|
||||||
|
const removeText = document.createTextNode(" remove");
|
||||||
|
remove.appendChild(removeText);
|
||||||
|
desc.appendChild(remove);
|
||||||
|
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove proto item based on it's ID
|
||||||
|
function removeProtoItem(name) {
|
||||||
|
const item = document.getElementById(name);
|
||||||
|
item.parentNode.removeChild(item);
|
||||||
|
protoMap.delete(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch all proto from protoMap
|
||||||
|
// compose it into formData to make it easier to send via ajax
|
||||||
|
function getProtos() {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
for (const proto of protoMap.values()) {
|
||||||
|
formData.append("protos", proto, proto.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return formData;
|
||||||
|
}
|
|
@ -9,15 +9,18 @@ $('#get-services').click(function(){
|
||||||
restart = "1"
|
restart = "1"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (target != t || restart == "1") {
|
// determine whether the proto connection will use local proto or not
|
||||||
|
const use_proto = $('#local-proto').is(":checked");
|
||||||
|
|
||||||
|
if (target != t || restart == "1" || use_proto) {
|
||||||
target = t;
|
target = t;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$('.other-elem').hide();
|
// prepare ajax options beforehand
|
||||||
var button = $(this).html();
|
// makes it easier for local proto to modify some of its properties
|
||||||
$.ajax({
|
const ajaxProps = {
|
||||||
url: "server/"+target+"/services?restart="+restart,
|
url: "server/"+target+"/services?restart="+restart,
|
||||||
global: true,
|
global: true,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
@ -48,7 +51,21 @@ $('#get-services').click(function(){
|
||||||
$(this).html(button);
|
$(this).html(button);
|
||||||
hide_loading();
|
hide_loading();
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// modify ajax options if use local proto
|
||||||
|
if (use_proto) {
|
||||||
|
ajaxProps.method = "POST";
|
||||||
|
ajaxProps.enctype = "multipart/form-data";
|
||||||
|
ajaxProps.processData = false;
|
||||||
|
ajaxProps.contentType = false;
|
||||||
|
ajaxProps.cache = false;
|
||||||
|
ajaxProps.data = getProtos();
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.other-elem').hide();
|
||||||
|
var button = $(this).html();
|
||||||
|
$.ajax(ajaxProps);
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#select-service').change(function(){
|
$('#select-service').change(function(){
|
||||||
|
|
Loading…
Reference in New Issue
Block a user