Skip to content

Commit

Permalink
Feat: use endpoint path instead of using header for routing external … (
Browse files Browse the repository at this point in the history
#56)

* Feat: use endpoint path instead of using header for routing external provider fn

Signed-off-by: Yin Da <yd219913@alibaba-inc.com>

* Feat: add external provider server

Signed-off-by: Yin Da <yd219913@alibaba-inc.com>

---------

Signed-off-by: Yin Da <yd219913@alibaba-inc.com>
  • Loading branch information
Somefive committed Feb 23, 2023
1 parent 3db6aa6 commit fc49c73
Show file tree
Hide file tree
Showing 3 changed files with 248 additions and 5 deletions.
128 changes: 128 additions & 0 deletions cue/cuex/externalserver/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
Copyright 2023 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package externalserver

import (
"context"
"encoding/json"
"io"
"net/http"

"github.com/emicklei/go-restful/v3"
"github.com/spf13/cobra"
"github.com/spf13/pflag"

"github.com/kubevela/pkg/util/cert"
)

// ServerProviderFn the function interface to process rest call
type ServerProviderFn interface {
Call(request *restful.Request, response *restful.Response)
}

// GenericServerProviderFn generic function that implements ServerProviderFn interface
type GenericServerProviderFn[T any, U any] func(context.Context, *T) (*U, error)

// Call handle rest call for given request
func (fn GenericServerProviderFn[T, U]) Call(request *restful.Request, response *restful.Response) {
bs, err := io.ReadAll(request.Request.Body)
if err != nil {
_ = response.WriteError(http.StatusBadRequest, err)
return
}
params := new(T)
if err = json.Unmarshal(bs, params); err != nil {
_ = response.WriteError(http.StatusBadRequest, err)
return
}
ret, err := fn(request.Request.Context(), params)
if err != nil {
_ = response.WriteError(http.StatusInternalServerError, err)
return
}
if bs, err = json.Marshal(ret); err != nil {
_ = response.WriteError(http.StatusInternalServerError, err)
return
}
_, _ = response.Write(bs)
return
}

const defaultAddr = ":8443"

// Server the external provider server
type Server struct {
Fns map[string]ServerProviderFn
Container *restful.Container

Addr string
TLS bool
CertFile string
KeyFile string
}

// ListenAndServe start the server
func (in *Server) ListenAndServe() (err error) {
if in.TLS && (in.CertFile == "" || in.KeyFile == "") {
in.CertFile, in.KeyFile, err = cert.GenerateDefaultSelfSignedCertificateLocally()
if err != nil {
return err
}
}
svr := &http.Server{Addr: in.Addr, Handler: in.Container}
if in.TLS {
return svr.ListenAndServeTLS(in.CertFile, in.KeyFile)
}
return svr.ListenAndServe()
}

// AddFlags set flags
func (in *Server) AddFlags(set *pflag.FlagSet) {
set.StringVarP(&in.Addr, "addr", "", in.Addr, "address of the server")
set.BoolVarP(&in.TLS, "tls", "", in.TLS, "enable tls server")
set.StringVarP(&in.CertFile, "cert-file", "", in.CertFile, "tls certificate path")
set.StringVarP(&in.KeyFile, "key-file", "", in.KeyFile, "tls key path")
}

// NewCommand create start command
func (in *Server) NewCommand() *cobra.Command {
cmd := &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
return in.ListenAndServe()
},
}
in.AddFlags(cmd.Flags())
return cmd
}

// NewServer create a server for serving as cuex external
func NewServer(path string, fns map[string]ServerProviderFn) *Server {
container := restful.NewContainer()
ws := &restful.WebService{}
ws.Path(path).Consumes(restful.MIME_JSON).Produces(restful.MIME_JSON)
for name, fn := range fns {
ws.Route(ws.POST(name).To(fn.Call))
}
container.Add(ws)
return &Server{
Fns: fns,
Container: container,

Addr: defaultAddr,
TLS: true,
}
}
117 changes: 117 additions & 0 deletions cue/cuex/externalserver/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
Copyright 2023 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package externalserver_test

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/emicklei/go-restful/v3"
"github.com/stretchr/testify/require"

"github.com/kubevela/pkg/cue/cuex/externalserver"
cuexruntime "github.com/kubevela/pkg/cue/cuex/runtime"
)

type val struct {
V string `json:"v"`
}

func (in *val) MarshalJSON() ([]byte, error) {
if in.V == "err_bar" {
return nil, fmt.Errorf(in.V)
}
return json.Marshal(map[string]string{"v": in.V})
}

func foo(ctx context.Context, input *val) (*val, error) {
if input.V == "err" {
return nil, fmt.Errorf(input.V)
}
return &val{V: "foo"}, nil
}

func bar(ctx context.Context, input *val) (*val, error) {
return &val{V: input.V + "_bar"}, nil
}

func TestExternalServer(t *testing.T) {
server := externalserver.NewServer("/", map[string]externalserver.ServerProviderFn{
"foo": externalserver.GenericServerProviderFn[val, val](foo),
"bar": externalserver.GenericServerProviderFn[val, val](bar),
})
cmd := server.NewCommand()
require.NoError(t, cmd.ParseFlags([]string{`--addr=:0`}))
go func() {
_ = cmd.Execute()
}()
time.Sleep(3 * time.Second)
svr := httptest.NewTLSServer(server.Container)
defer svr.Close()
for name, tt := range map[string]struct {
Path string
Input string
Output string
StatusCode int
}{
"foo": {
Path: "/foo",
Input: `{"v":"value"}`,
Output: `{"v":"foo"}`,
StatusCode: 200,
},
"bar": {
Path: "/bar",
Input: `{"v":"value"}`,
Output: `{"v":"value_bar"}`,
StatusCode: 200,
},
"bad-json": {
Path: "/foo",
Input: `{bad`,
StatusCode: 400,
},
"bad-ret": {
Path: "/bar",
Input: `{"v":"err"}`,
StatusCode: 500,
},
"foo-err": {
Path: "/foo",
Input: `{"v":"err"}`,
StatusCode: 500,
},
} {
t.Run(name, func(t *testing.T) {
resp, err := cuexruntime.DefaultClient.Get().Post(svr.URL+tt.Path, restful.MIME_JSON, bytes.NewReader([]byte(tt.Input)))
require.NoError(t, err)
require.Equal(t, tt.StatusCode, resp.StatusCode)
if tt.StatusCode == http.StatusOK {
bs, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, []byte(tt.Output), bs)
}
})
}
}
8 changes: 3 additions & 5 deletions cue/cuex/runtime/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"fmt"
"io"
"net/http"
"strings"

"cuelang.org/go/cue"
"k8s.io/apimachinery/pkg/runtime"
Expand Down Expand Up @@ -77,9 +78,6 @@ var DefaultClient = singleton.NewSingleton(func() *http.Client {
}
})

// FunctionHeaderKey http header for recording cuex provider function
const FunctionHeaderKey = "CueX-External-Provider-Function"

// Call dial external endpoints by passing the json data of the input parameter,
// then fill back returned values
func (in *ExternalProviderFn) Call(ctx context.Context, value cue.Value) (cue.Value, error) {
Expand All @@ -90,12 +88,12 @@ func (in *ExternalProviderFn) Call(ctx context.Context, value cue.Value) (cue.Va
}
switch in.Protocol {
case v1alpha1.ProtocolHTTP, v1alpha1.ProtocolHTTPS:
req, err := http.NewRequest(http.MethodPost, in.Endpoint, bytes.NewReader(bs))
ep := fmt.Sprintf("%s/%s", strings.TrimSuffix(in.Endpoint, "/"), in.Fn)
req, err := http.NewRequest(http.MethodPost, ep, bytes.NewReader(bs))
if err != nil {
return value, err
}
req.Header.Set("Content-Type", runtime.ContentTypeJSON)
req.Header.Set(FunctionHeaderKey, in.Fn)
resp, err := DefaultClient.Get().Do(req.WithContext(ctx))
if err != nil {
return value, err
Expand Down

0 comments on commit fc49c73

Please sign in to comment.