From 0245e46a847cdf8c5ccbddf6d56168ff01e03a69 Mon Sep 17 00:00:00 2001 From: Sarasa Kisaragi Date: Tue, 22 Aug 2023 17:49:56 +0800 Subject: [PATCH] feat: simple APISIX sdk (#4) * feat: simple apisix sdk Signed-off-by: Ling Samuel (WSL) * rollback changes * fmt code Signed-off-by: Ling Samuel (WSL) * rollback types changes * simple rename Signed-off-by: Ling Samuel (WSL) * generic client * fix service types Signed-off-by: Ling Samuel (WSL) * remove v2 version admin API Signed-off-by: Ling Samuel (WSL) * feat: add route api Signed-off-by: Ling Samuel (WSL) * remove upstream api Signed-off-by: Ling Samuel (WSL) --------- Signed-off-by: Ling Samuel (WSL) --- Makefile | 4 + cmd/configure.go | 2 +- cmd/dump.go | 28 +++- cmd/ping.go | 2 +- cmd/sync.go | 2 +- go.mod | 2 + go.sum | 7 + internal/pkg/db/memdb.go | 3 +- pkg/api/apisix/apisix.go | 28 ++++ pkg/api/apisix/client.go | 226 ++++++++++++++++++++++++++++++ pkg/api/apisix/cluster.go | 40 ++++++ pkg/api/apisix/resource.go | 87 ++++++++++++ pkg/api/apisix/resource_client.go | 93 ++++++++++++ pkg/api/apisix/route.go | 26 ++++ pkg/api/apisix/service.go | 26 ++++ pkg/api/apisix/types/types.go | 224 +++++++++++++++++++++++++++++ pkg/data/types.go | 74 +++++++++- utils/goimports-reviser.sh | 8 ++ 18 files changed, 876 insertions(+), 6 deletions(-) create mode 100644 pkg/api/apisix/apisix.go create mode 100644 pkg/api/apisix/client.go create mode 100644 pkg/api/apisix/cluster.go create mode 100644 pkg/api/apisix/resource.go create mode 100644 pkg/api/apisix/resource_client.go create mode 100644 pkg/api/apisix/route.go create mode 100644 pkg/api/apisix/service.go create mode 100644 pkg/api/apisix/types/types.go create mode 100755 utils/goimports-reviser.sh diff --git a/Makefile b/Makefile index eb4f0e5..07e0b81 100644 --- a/Makefile +++ b/Makefile @@ -12,3 +12,7 @@ build: ## Build adc test: @go test -v ./... .PHONY: test + +.PHONY: fmt +fmt: ## Format all go codes + ./utils/goimports-reviser.sh diff --git a/cmd/configure.go b/cmd/configure.go index d14669a..689e8f4 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -56,6 +56,6 @@ func saveConfiguration() error { return err } - color.Green("Scucessfully configure ADC") + color.Green("Successfully configure ADC") return nil } diff --git a/cmd/dump.go b/cmd/dump.go index 7b06385..c609d33 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -4,8 +4,14 @@ Copyright © 2023 API7.ai package cmd import ( + "context" + "encoding/json" + "fmt" + "github.com/fatih/color" "github.com/spf13/cobra" + + "github.com/api7/adc/pkg/api/apisix" ) // newDumpCmd represents the dump command @@ -15,7 +21,9 @@ func newDumpCmd() *cobra.Command { Short: "Dump the configurations of API7", Long: `The dump command can be used to dump the configurations to the API7.`, RunE: func(cmd *cobra.Command, args []string) error { - color.Green("Scucessfully dump configurations") + testClient() + + color.Green("Successfully dump configurations") return nil }, } @@ -24,3 +32,21 @@ func newDumpCmd() *cobra.Command { return cmd } + +func testClient() { + cluster := apisix.NewCluster(context.Background(), rootConfig.Server, rootConfig.Token) + + ups, err := cluster.Service().List(context.Background()) + if err != nil { + color.Red(err.Error()) + return + } + + data, err := json.MarshalIndent(ups, "", " ") + if err != nil { + color.Red(err.Error()) + return + } + + fmt.Println(string(data)) +} diff --git a/cmd/ping.go b/cmd/ping.go index 03b8b6f..02f03db 100644 --- a/cmd/ping.go +++ b/cmd/ping.go @@ -46,7 +46,7 @@ func pingAPI7() error { defer resp.Body.Close() if resp.StatusCode == http.StatusOK { - color.Green("Scucessfully connected to the API7") + color.Green("Successfully connected to the API7") } else { color.Red("Failed to connect to the API7") } diff --git a/cmd/sync.go b/cmd/sync.go index 9b89237..7d0122e 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -30,7 +30,7 @@ func newSyncCmd() *cobra.Command { } color.Green("Get file content success: %v", d) - color.Green("Scucessfully sync configurations") + color.Green("Successfully sync configurations") return nil }, } diff --git a/go.mod b/go.mod index 89ede46..2f95e7a 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/spf13/cobra v1.7.0 github.com/spf13/viper v1.16.0 github.com/stretchr/testify v1.8.4 + go.uber.org/multierr v1.8.0 sigs.k8s.io/yaml v1.3.0 ) @@ -31,6 +32,7 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.4.2 // indirect + go.uber.org/atomic v1.9.0 // indirect golang.org/x/sys v0.8.0 // indirect golang.org/x/text v0.9.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 6957fe5..1f80c43 100644 --- a/go.sum +++ b/go.sum @@ -184,6 +184,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -204,6 +205,11 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= +go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -498,6 +504,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/pkg/db/memdb.go b/internal/pkg/db/memdb.go index d7188c0..17c38a3 100644 --- a/internal/pkg/db/memdb.go +++ b/internal/pkg/db/memdb.go @@ -3,8 +3,9 @@ package db import ( "errors" - "github.com/api7/adc/pkg/data" "github.com/hashicorp/go-memdb" + + "github.com/api7/adc/pkg/data" ) var schema = &memdb.DBSchema{ diff --git a/pkg/api/apisix/apisix.go b/pkg/api/apisix/apisix.go new file mode 100644 index 0000000..2951d9d --- /dev/null +++ b/pkg/api/apisix/apisix.go @@ -0,0 +1,28 @@ +package apisix + +import ( + "context" + + "github.com/api7/adc/pkg/api/apisix/types" +) + +type Cluster interface { + Route() Route + Service() Service +} + +type Route interface { + Get(ctx context.Context, name string) (*types.Route, error) + List(ctx context.Context) ([]*types.Route, error) + Create(ctx context.Context, ups *types.Route) (*types.Route, error) + Delete(ctx context.Context, name string) error + Update(ctx context.Context, ups *types.Route) (*types.Route, error) +} + +type Service interface { + Get(ctx context.Context, name string) (*types.Service, error) + List(ctx context.Context) ([]*types.Service, error) + Create(ctx context.Context, ups *types.Service) (*types.Service, error) + Delete(ctx context.Context, name string) error + Update(ctx context.Context, ups *types.Service) (*types.Service, error) +} diff --git a/pkg/api/apisix/client.go b/pkg/api/apisix/client.go new file mode 100644 index 0000000..9b94d41 --- /dev/null +++ b/pkg/api/apisix/client.go @@ -0,0 +1,226 @@ +package apisix + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + "time" + + "go.uber.org/multierr" +) + +var ( + ErrNotFound = fmt.Errorf("not found") + ErrStillInUse = errors.New("still in use") // We should use force mode + ErrFunctionDisabled = errors.New("function disabled") +) + +type Client struct { + baseURL string + adminKey string + + cli *http.Client +} + +func newClient(baseURL, adminKey string) *Client { + return &Client{ + baseURL: baseURL, + adminKey: adminKey, + cli: &http.Client{ + Timeout: 5 * time.Second, + }, + } +} + +func (c *Client) setAdminKey(req *http.Request) { + if c.adminKey != "" { + req.Header.Set("X-API-Key", c.adminKey) + } +} + +func (c *Client) do(req *http.Request) (*http.Response, error) { + c.setAdminKey(req) + return c.cli.Do(req) +} + +func (c *Client) isFunctionDisabled(body string) bool { + return strings.Contains(body, "is disabled") +} + +func (c *Client) getResource(ctx context.Context, url string) (*item, error) { + var res getResponse + err := makeGetRequest(c, ctx, url, &res) + if err != nil { + return nil, err + } + return &res, nil +} + +func (c *Client) listResource(ctx context.Context, url string) (items, error) { + var res listResponse + + err := makeGetRequest(c, ctx, url, &res) + if err != nil { + return nil, err + } + return res.List, nil +} + +func (c *Client) createResource(ctx context.Context, url string, body []byte) (*item, error) { + var cr createResponse + err := makePutRequest(c, ctx, url, body, &cr) + if err != nil { + return nil, err + } + return &cr, nil +} + +func (c *Client) updateResource(ctx context.Context, url string, body []byte) (*item, error) { + var ur updateResponse + + err := makePutRequest(c, ctx, url, body, &ur) + if err != nil { + return nil, err + } + return &ur, nil +} + +func (c *Client) deleteResource(ctx context.Context, url string) error { + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil) + if err != nil { + return err + } + resp, err := c.do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusNotFound { + message := readBody(resp.Body) + if c.isFunctionDisabled(message) { + return ErrFunctionDisabled + } + err = multierr.Append(err, fmt.Errorf("unexpected status code %d", resp.StatusCode)) + err = multierr.Append(err, fmt.Errorf("error message: %s", message)) + if strings.Contains(message, "still using") { + return ErrStillInUse + } + return err + } + return nil +} + +func readBody(r io.ReadCloser) string { + defer r.Close() + + data, err := io.ReadAll(r) + if err != nil { + return "" + } + return string(data) +} + +// getSchema returns the schema of APISIX object. +func (c *Client) getSchema(ctx context.Context, url string) (string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return "", err + } + resp, err := c.do(req) + if err != nil { + return "", err + } + + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + if resp.StatusCode == http.StatusNotFound { + return "", ErrNotFound + } else { + err = multierr.Append(err, fmt.Errorf("unexpected status code %d", resp.StatusCode)) + err = multierr.Append(err, fmt.Errorf("error message: %s", readBody(resp.Body))) + } + return "", err + } + + return readBody(resp.Body), nil +} + +// getList returns a list of string. +func (c *Client) getList(ctx context.Context, url string) ([]string, error) { + var listResp map[string]interface{} + err := makeGetRequest(c, ctx, url, &listResp) + if err != nil { + return nil, err + } + res := make([]string, 0, len(listResp)) + + for name := range listResp { + res = append(res, name) + } + return res, nil +} + +func makeGetRequest[T any](c *Client, ctx context.Context, url string, result *T) error { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return err + } + resp, err := c.do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + body := readBody(resp.Body) + if c.isFunctionDisabled(body) { + return ErrFunctionDisabled + } + err = multierr.Append(err, fmt.Errorf("unexpected status code %d", resp.StatusCode)) + err = multierr.Append(err, fmt.Errorf("error message: %s", body)) + return err + } + + dec := json.NewDecoder(resp.Body) + if err := dec.Decode(result); err != nil { + return err + } + + return nil +} + +func makePutRequest[T any](c *Client, ctx context.Context, url string, body []byte, result *T) error { + req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, bytes.NewReader(body)) + if err != nil { + return err + } + resp, err := c.do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + body := readBody(resp.Body) + if c.isFunctionDisabled(body) { + return ErrFunctionDisabled + } + err = multierr.Append(err, fmt.Errorf("unexpected status code %d", resp.StatusCode)) + err = multierr.Append(err, fmt.Errorf("error message: %s", body)) + return err + } + dec := json.NewDecoder(resp.Body) + if err := dec.Decode(result); err != nil { + return err + } + + return nil +} diff --git a/pkg/api/apisix/cluster.go b/pkg/api/apisix/cluster.go new file mode 100644 index 0000000..84ceb46 --- /dev/null +++ b/pkg/api/apisix/cluster.go @@ -0,0 +1,40 @@ +package apisix + +import ( + "context" +) + +type cluster struct { + baseURL string + adminKey string + + cli *Client + + route Route + service Service +} + +func NewCluster(ctx context.Context, url, adminKey string) Cluster { + c := &cluster{ + baseURL: url, + adminKey: adminKey, + } + + cli := newClient(url, adminKey) + c.cli = cli + + c.route = newRoute(cli) + c.service = newService(cli) + + return c +} + +// Route implements Cluster.Route method. +func (c *cluster) Route() Route { + return c.route +} + +// Service implements Cluster.Service method. +func (c *cluster) Service() Service { + return c.service +} diff --git a/pkg/api/apisix/resource.go b/pkg/api/apisix/resource.go new file mode 100644 index 0000000..c9ac036 --- /dev/null +++ b/pkg/api/apisix/resource.go @@ -0,0 +1,87 @@ +package apisix + +import ( + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + + "github.com/api7/adc/pkg/api/apisix/types" +) + +type getResponse = item + +type createResponse = item + +type updateResponse = item + +// listResponse is the v3 version unified LIST response mapping of APISIX. +type listResponse struct { + Total IntOrString `json:"total"` + List items `json:"list"` +} + +// IntOrString processing number and string types, after json deserialization will output int +type IntOrString struct { + IntValue int `json:"int_value"` +} + +func (ios *IntOrString) UnmarshalJSON(p []byte) error { + result := strings.Trim(string(p), "\"") + count, err := strconv.Atoi(result) + if err != nil { + return err + } + ios.IntValue = count + return nil +} + +type items []item + +// UnmarshalJSON implements json.Unmarshaler interface. +// lua-cjson doesn't distinguish empty array and table, +// and by default empty array will be encoded as '{}'. +// We have to maintain the compatibility. +func (items *items) UnmarshalJSON(p []byte) error { + if p[0] == '{' { + if len(p) != 2 { + return errors.New("unexpected non-empty object") + } + return nil + } + var data []item + if err := json.Unmarshal(p, &data); err != nil { + return err + } + *items = data + return nil +} + +type item struct { + Key string `json:"key"` + Value json.RawMessage `json:"value"` +} + +func unmarshalItem[T any](i *item) (*T, error) { + list := strings.Split(i.Key, "/") + if len(list) < 1 { + return nil, fmt.Errorf("bad upstream config key: %s", i.Key) + } + + var ups T + if err := json.Unmarshal(i.Value, &ups); err != nil { + return nil, err + } + return &ups, nil +} + +// upstream decodes item.Value and converts it to types.Upstream. +func (i *item) upstream() (*types.Upstream, error) { + return unmarshalItem[types.Upstream](i) +} + +// service decodes item.Value and converts it to types.Service. +func (i *item) service() (*types.Service, error) { + return unmarshalItem[types.Service](i) +} diff --git a/pkg/api/apisix/resource_client.go b/pkg/api/apisix/resource_client.go new file mode 100644 index 0000000..7c86cd9 --- /dev/null +++ b/pkg/api/apisix/resource_client.go @@ -0,0 +1,93 @@ +package apisix + +import ( + "context" + "encoding/json" +) + +type resourceClient[T any] struct { + url string + client *Client +} + +func newResourceClient[T any](c *Client, resourceName string) *resourceClient[T] { + return &resourceClient[T]{ + url: c.baseURL + "/" + resourceName, + client: c, + } +} + +func (u *resourceClient[T]) Get(ctx context.Context, name string) (*T, error) { + url := u.url + "/" + name + resp, err := u.client.getResource(ctx, url) + if err != nil { + return nil, err + } + + ups, err := unmarshalItem[T](resp) + if err != nil { + return nil, err + } + return ups, nil +} + +func (u *resourceClient[T]) List(ctx context.Context) ([]*T, error) { + svcItems, err := u.client.listResource(ctx, u.url) + if err != nil { + return nil, err + } + + var items []*T + for _, item := range svcItems { + svc, err := unmarshalItem[T](&item) + if err != nil { + return nil, err + } + items = append(items, svc) + } + return items, nil +} + +func (u *resourceClient[T]) Create(ctx context.Context, id string, obj *T) (*T, error) { + body, err := json.Marshal(obj) + if err != nil { + return nil, err + } + url := u.url + "/" + id + + resp, err := u.client.createResource(ctx, url, body) + if err != nil { + return nil, err + } + svc, err := unmarshalItem[T](resp) + if err != nil { + return nil, err + } + return svc, err +} + +func (u *resourceClient[T]) Delete(ctx context.Context, name string) error { + url := u.url + "/" + name + if err := u.client.deleteResource(ctx, url); err != nil { + return err + } + return nil +} + +func (u *resourceClient[T]) Update(ctx context.Context, id string, obj *T) (*T, error) { + body, err := json.Marshal(obj) + if err != nil { + return nil, err + } + + url := u.url + "/" + id + resp, err := u.client.updateResource(ctx, url, body) + if err != nil { + return nil, err + } + svc, err := unmarshalItem[T](resp) + if err != nil { + return nil, err + } + return svc, err +} diff --git a/pkg/api/apisix/route.go b/pkg/api/apisix/route.go new file mode 100644 index 0000000..5fb1e2d --- /dev/null +++ b/pkg/api/apisix/route.go @@ -0,0 +1,26 @@ +package apisix + +import ( + "context" + + "github.com/api7/adc/pkg/api/apisix/types" +) + +type routeClient struct { + *resourceClient[types.Route] +} + +func newRoute(c *Client) Route { + cli := newResourceClient[types.Route](c, "routes") + return &routeClient{ + resourceClient: cli, + } +} + +func (u *routeClient) Create(ctx context.Context, obj *types.Route) (*types.Route, error) { + return u.resourceClient.Create(ctx, obj.ID, obj) +} + +func (u *routeClient) Update(ctx context.Context, obj *types.Route) (*types.Route, error) { + return u.resourceClient.Update(ctx, obj.ID, obj) +} diff --git a/pkg/api/apisix/service.go b/pkg/api/apisix/service.go new file mode 100644 index 0000000..d711d15 --- /dev/null +++ b/pkg/api/apisix/service.go @@ -0,0 +1,26 @@ +package apisix + +import ( + "context" + + "github.com/api7/adc/pkg/api/apisix/types" +) + +type serviceClient struct { + *resourceClient[types.Service] +} + +func newService(c *Client) Service { + cli := newResourceClient[types.Service](c, "services") + return &serviceClient{ + resourceClient: cli, + } +} + +func (u *serviceClient) Create(ctx context.Context, obj *types.Service) (*types.Service, error) { + return u.resourceClient.Create(ctx, obj.ID, obj) +} + +func (u *serviceClient) Update(ctx context.Context, obj *types.Service) (*types.Service, error) { + return u.resourceClient.Update(ctx, obj.ID, obj) +} diff --git a/pkg/api/apisix/types/types.go b/pkg/api/apisix/types/types.go new file mode 100644 index 0000000..5311ff3 --- /dev/null +++ b/pkg/api/apisix/types/types.go @@ -0,0 +1,224 @@ +package types + +import ( + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + + "github.com/api7/adc/pkg/data" +) + +// Route apisix route object +// +k8s:deepcopy-gen=true +type Route struct { + ID string `json:"id" yaml:"id"` + + Labels data.StringArray `json:"labels,omitempty" yaml:"labels,omitempty"` + + Host string `json:"host,omitempty" yaml:"host,omitempty"` + Hosts []string `json:"hosts,omitempty" yaml:"hosts,omitempty"` + Uri string `json:"uri,omitempty" yaml:"uri,omitempty"` + Priority int `json:"priority,omitempty" yaml:"priority,omitempty"` + Timeout *UpstreamTimeout `json:"timeout,omitempty" yaml:"timeout,omitempty"` + Vars data.Vars `json:"vars,omitempty" yaml:"vars,omitempty"` + Uris []string `json:"uris,omitempty" yaml:"uris,omitempty"` + Methods []string `json:"methods,omitempty" yaml:"methods,omitempty"` + EnableWebsocket bool `json:"enable_websocket,omitempty" yaml:"enable_websocket,omitempty"` + RemoteAddrs []string `json:"remote_addrs,omitempty" yaml:"remote_addrs,omitempty"` + UpstreamId string `json:"upstream_id,omitempty" yaml:"upstream_id,omitempty"` + Plugins data.Plugins `json:"plugins,omitempty" yaml:"plugins,omitempty"` + PluginConfigId string `json:"plugin_config_id,omitempty" yaml:"plugin_config_id,omitempty"` + FilterFunc string `json:"filter_func,omitempty" yaml:"filter_func,omitempty"` +} + +// Service is the abstraction of a backend service on API gateway. +type Service struct { + ID string `json:"id"` + + Description string `json:"desc,omitempty"` + // Labels are used for resource classification and indexing + Labels data.StringArray `json:"labels,omitempty"` + // HTTP hosts for this service. + Hosts []string `json:"hosts"` + // Plugin settings on Service level + Plugins data.Plugins `json:"plugins,omitempty"` + // Upstream settings for the Service. + Upstream Upstream `json:"upstream"` + // UpstreamId settings for the Service. + UpstreamId string `json:"upstream_id"` + // Enables a websocket. Set to false by default. + EnableWebsocket bool `json:"enable_websocket,omitempty"` +} + +// Upstream is the definition of the upstream on Service. +type Upstream struct { + // ID is the upstream name. It should be unique among all upstreams + // in the same service. + ID string `json:"id"` + + Type string `json:"type,omitempty" yaml:"type,omitempty"` + HashOn string `json:"hash_on,omitempty" yaml:"hash_on,omitempty"` + Key string `json:"key,omitempty" yaml:"key,omitempty"` + Checks *UpstreamHealthCheck `json:"checks,omitempty" yaml:"checks,omitempty"` + Nodes UpstreamNodes `json:"nodes" yaml:"nodes"` + Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"` + Retries *int `json:"retries,omitempty" yaml:"retries,omitempty"` + Timeout *UpstreamTimeout `json:"timeout,omitempty" yaml:"timeout,omitempty"` + TLS *ClientTLS `json:"tls,omitempty" yaml:"tls,omitempty"` + + // for Service Discovery + ServiceName string `json:"service_name,omitempty" yaml:"service_name,omitempty"` + DiscoveryType string `json:"discovery_type,omitempty" yaml:"discovery_type,omitempty"` + DiscoveryArgs map[string]string `json:"discovery_args,omitempty" yaml:"discovery_args,omitempty"` +} + +// UpstreamNode is the node in upstream +// +k8s:deepcopy-gen=true +type UpstreamNode struct { + Host string `json:"host,omitempty" yaml:"host,omitempty"` + Port int `json:"port,omitempty" yaml:"port,omitempty"` + Weight int `json:"weight,omitempty" yaml:"weight,omitempty"` +} + +// UpstreamNodes is the upstream node list. +type UpstreamNodes []UpstreamNode + +// UnmarshalJSON implements json.Unmarshaler interface. +// lua-cjson doesn't distinguish empty array and table, +// and by default empty array will be encoded as '{}'. +// We have to maintain the compatibility. +func (n *UpstreamNodes) UnmarshalJSON(p []byte) error { + var data []UpstreamNode + if p[0] == '{' { + value := map[string]float64{} + if err := json.Unmarshal(p, &value); err != nil { + return err + } + for k, v := range value { + node, err := mapKV2Node(k, v) + if err != nil { + return err + } + data = append(data, *node) + } + *n = data + return nil + } + if err := json.Unmarshal(p, &data); err != nil { + return err + } + *n = data + return nil +} + +// UpstreamHealthCheck defines the active and/or passive health check for an Upstream, +// with the upstream health check feature, pods can be kicked out or joined in quickly, +// if the feedback of Kubernetes liveness/readiness probe is long. +// +k8s:deepcopy-gen=true +type UpstreamHealthCheck struct { + Active *UpstreamActiveHealthCheck `json:"active" yaml:"active"` + Passive *UpstreamPassiveHealthCheck `json:"passive,omitempty" yaml:"passive,omitempty"` +} + +// UpstreamActiveHealthCheck defines the active kind of upstream health check. +// +k8s:deepcopy-gen=true +type UpstreamActiveHealthCheck struct { + Type string `json:"type,omitempty" yaml:"type,omitempty"` + Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"` + Concurrency int `json:"concurrency,omitempty" yaml:"concurrency,omitempty"` + Host string `json:"host,omitempty" yaml:"host,omitempty"` + Port int32 `json:"port,omitempty" yaml:"port,omitempty"` + HTTPPath string `json:"http_path,omitempty" yaml:"http_path,omitempty"` + HTTPSVerifyCert bool `json:"https_verify_certificate,omitempty" yaml:"https_verify_certificate,omitempty"` + HTTPRequestHeaders []string `json:"req_headers,omitempty" yaml:"req_headers,omitempty"` + Healthy UpstreamActiveHealthCheckHealthy `json:"healthy,omitempty" yaml:"healthy,omitempty"` + Unhealthy UpstreamActiveHealthCheckUnhealthy `json:"unhealthy,omitempty" yaml:"unhealthy,omitempty"` +} + +// UpstreamPassiveHealthCheck defines the passive kind of upstream health check. +// +k8s:deepcopy-gen=true +type UpstreamPassiveHealthCheck struct { + Type string `json:"type,omitempty" yaml:"type,omitempty"` + Healthy UpstreamPassiveHealthCheckHealthy `json:"healthy,omitempty" yaml:"healthy,omitempty"` + Unhealthy UpstreamPassiveHealthCheckUnhealthy `json:"unhealthy,omitempty" yaml:"unhealthy,omitempty"` +} + +// UpstreamActiveHealthCheckHealthy defines the conditions to judge whether +// an upstream node is healthy with the active manner. +// +k8s:deepcopy-gen=true +type UpstreamActiveHealthCheckHealthy struct { + UpstreamPassiveHealthCheckHealthy `json:",inline" yaml:",inline"` + + Interval int `json:"interval,omitempty" yaml:"interval,omitempty"` +} + +// UpstreamPassiveHealthCheckHealthy defines the conditions to judge whether +// an upstream node is healthy with the passive manner. +// +k8s:deepcopy-gen=true +type UpstreamPassiveHealthCheckHealthy struct { + HTTPStatuses []int `json:"http_statuses,omitempty" yaml:"http_statuses,omitempty"` + Successes int `json:"successes,omitempty" yaml:"successes,omitempty"` +} + +// UpstreamActiveHealthCheckUnhealthy defines the conditions to judge whether +// an upstream node is unhealthy with the active manager. +// +k8s:deepcopy-gen=true +type UpstreamActiveHealthCheckUnhealthy struct { + UpstreamPassiveHealthCheckUnhealthy `json:",inline" yaml:",inline"` + + Interval int `json:"interval,omitempty" yaml:"interval,omitempty"` +} + +// UpstreamPassiveHealthCheckUnhealthy defines the conditions to judge whether +// an upstream node is unhealthy with the passive manager. +// +k8s:deepcopy-gen=true +type UpstreamPassiveHealthCheckUnhealthy struct { + HTTPStatuses []int `json:"http_statuses,omitempty" yaml:"http_statuses,omitempty"` + HTTPFailures int `json:"http_failures,omitempty" yaml:"http_failures,omitempty"` + TCPFailures int `json:"tcp_failures,omitempty" yaml:"tcp_failures,omitempty"` + Timeouts int `json:"timeouts,omitempty" yaml:"timeouts,omitempty"` +} + +// ClientTLS is tls cert and key use in mTLS +type ClientTLS struct { + Cert string `json:"client_cert,omitempty" yaml:"client_cert,omitempty"` + Key string `json:"client_key,omitempty" yaml:"client_key,omitempty"` +} + +// UpstreamTimeout represents the timeout settings on Upstream. +type UpstreamTimeout struct { + // Connect is the connect timeout + Connect int `json:"connect" yaml:"connect"` + // Send is the send timeout + Send int `json:"send" yaml:"send"` + // Read is the read timeout + Read int `json:"read" yaml:"read"` +} + +func mapKV2Node(key string, val float64) (*UpstreamNode, error) { + hp := strings.Split(key, ":") + host := hp[0] + // according to APISIX upstream nodes policy, port is required + port := "80" + + if len(hp) > 2 { + return nil, errors.New("invalid upstream node") + } else if len(hp) == 2 { + port = hp[1] + } + + portInt, err := strconv.Atoi(port) + if err != nil { + return nil, fmt.Errorf("parse port to int fail: %s", err.Error()) + } + + node := &UpstreamNode{ + Host: host, + Port: portInt, + Weight: int(val), + } + + return node, nil +} diff --git a/pkg/data/types.go b/pkg/data/types.go index be155b9..b557b0f 100644 --- a/pkg/data/types.go +++ b/pkg/data/types.go @@ -1,15 +1,87 @@ package data import ( + "encoding/json" + "errors" "fmt" ) // StringArray is enhanced version of pq.StringArray that can be handled nil value automatically. type StringArray []string -// Plugins contains a collect of polices like CORS. +// Vars represents the route match expressions of APISIX. +type Vars [][]StringOrSlice + +// UnmarshalJSON implements json.Unmarshaler interface. +// lua-cjson doesn't distinguish empty array and table, +// and by default empty array will be encoded as '{}'. +// We have to maintain the compatibility. +func (vars *Vars) UnmarshalJSON(p []byte) error { + if p[0] == '{' { + if len(p) != 2 { + return errors.New("unexpected non-empty object") + } + return nil + } + var data [][]StringOrSlice + if err := json.Unmarshal(p, &data); err != nil { + return err + } + *vars = data + return nil +} + +// StringOrSlice represents a string or a string slice. +// TODO Do not use interface{} to avoid the reflection overheads. +// +k8s:deepcopy-gen=true +type StringOrSlice struct { + StrVal string `json:"-"` + SliceVal []string `json:"-"` +} + +func (s *StringOrSlice) MarshalJSON() ([]byte, error) { + var ( + p []byte + err error + ) + if s.SliceVal != nil { + p, err = json.Marshal(s.SliceVal) + } else { + p, err = json.Marshal(s.StrVal) + } + return p, err +} + +func (s *StringOrSlice) UnmarshalJSON(p []byte) error { + var err error + + if len(p) == 0 { + return errors.New("empty object") + } + if p[0] == '[' { + err = json.Unmarshal(p, &s.SliceVal) + } else { + err = json.Unmarshal(p, &s.StrVal) + } + return err +} + type Plugins map[string]interface{} +func (p *Plugins) DeepCopyInto(out *Plugins) { + b, _ := json.Marshal(&p) + _ = json.Unmarshal(b, out) +} + +func (p *Plugins) DeepCopy() *Plugins { + if p == nil { + return nil + } + out := new(Plugins) + p.DeepCopyInto(out) + return out +} + // Service is the abstraction of a backend service on API gateway. type Service struct { ID string `json:"id"` diff --git a/utils/goimports-reviser.sh b/utils/goimports-reviser.sh new file mode 100755 index 0000000..dd03ed3 --- /dev/null +++ b/utils/goimports-reviser.sh @@ -0,0 +1,8 @@ +set -e + +go install github.com/incu6us/goimports-reviser/v2@latest + +PROJECT_NAME=github.com/api7/adc +while IFS= read -r -d '' file; do + goimports-reviser -file-path "$file" -project-name $PROJECT_NAME +done < <(find . -name '*.go' -print0)