Skip to content

Commit

Permalink
chore: apisix http client
Browse files Browse the repository at this point in the history
  • Loading branch information
tokers committed Dec 30, 2020
1 parent 782730a commit a6d92e9
Show file tree
Hide file tree
Showing 4 changed files with 491 additions and 0 deletions.
123 changes: 123 additions & 0 deletions pkg/apisix/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You 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 apisix

import (
"context"
"io"
"io/ioutil"
"net/http"
"time"

"github.com/api7/ingress-controller/pkg/log"
"go.uber.org/zap"

v1 "github.com/gxthrj/apisix-types/pkg/apis/apisix/v1"
)

const (
_defaultTimeout = 5 * time.Second
)

// Options contains parameters to customize APISIX client.
type Options struct {
AdminKey string
BaseURL string
Timeout time.Duration
}

// Interface is the unified client tool to communicate with APISIX.
type Client interface {
Route() Route
}

// Route is the specific client to take over the create, update, list and delete
// for APISIX's Route resource.
type Route interface {
List(context.Context, string) ([]*v1.Route, error)
Create(context.Context, *v1.Route, string) (*v1.Route, error)
Delete(context.Context, *v1.Route) error
Update(context.Context, *v1.Route) error
}

type client struct {
stub *stub
route Route
}

type stub struct {
baseURL string
adminKey string
cli *http.Client
}

func (s *stub) applyAuth(req *http.Request) {
if s.adminKey != "" {
req.Header.Set("X-API-Key", s.adminKey)
}
}

func (s *stub) do(req *http.Request) (*http.Response, error) {
s.applyAuth(req)
return s.cli.Do(req)
}

// NewClient creates an APISIX client to perform resources change pushing.
func NewClient(o *Options) Client {
if o.BaseURL == "" {
o.BaseURL = "/apisix/admin"
}
if o.Timeout == time.Duration(0) {
o.Timeout = _defaultTimeout
}
stub := &stub{
baseURL: o.BaseURL,
adminKey: o.AdminKey,
cli: &http.Client{
Timeout: o.Timeout,
Transport: &http.Transport{
ResponseHeaderTimeout: o.Timeout,
ExpectContinueTimeout: o.Timeout,
},
},
}
cli := &client{
stub: stub,
}
cli.route = newRouteClient(stub)
return cli
}

// Route implements Client interface.
func (c *client) Route() Route {
return c.route
}

func drainBody(r io.ReadCloser, url string) {
_, err := io.Copy(ioutil.Discard, r)
if err != nil {
log.Warnw("failed to drain body (read)",
zap.String("url", url),
zap.Error(err),
)
}

if err := r.Close(); err != nil {
log.Warnw("failed to drain body (close)",
zap.String("url", url),
zap.Error(err),
)
}
}
106 changes: 106 additions & 0 deletions pkg/apisix/resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You 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 apisix

import (
"encoding/json"
"errors"
"fmt"
"strings"

v1 "github.com/gxthrj/apisix-types/pkg/apis/apisix/v1"
)

// listRepsonse is the unified LIST response mapping of APISIX.
type listResponse struct {
Count string `json:"count"`
Node node `json:"node"`
}

type node struct {
Key string `json:"key"`
Items items `json:"nodes"`
}

type items []item

// items 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"`
}

type routeItem struct {
UpstreamId *string `json:"upstream_id"`
ServiceId *string `json:"service_id"`
Host *string `json:"host"`
URI *string `json:"uri"`
Desc *string `json:"desc"`
Methods []*string `json:"methods"`
Plugins map[string]interface{} `json:"plugins"`
}

// route decodes item.value and converts it to v1.Route.
func (i *item) route(group string) (*v1.Route, error) {
list := strings.Split(i.Key, "/")
if len(list) < 1 {
return nil, fmt.Errorf("bad route config key: %s", i.Key)
}

var route routeItem
if err := json.Unmarshal(i.Value, &route); err != nil {
return nil, err
}

name := route.Desc
fullName := "unknown"
if name != nil {
fullName = *name
}
if group != "" {
fullName = group + "_" + fullName
}

return &v1.Route{
ID: &list[len(list)-1],
Group: &group,
FullName: &fullName,
Name: route.Desc,
Host: route.Host,
Path: route.URI,
Methods: route.Methods,
UpstreamId: route.UpstreamId,
ServiceId: route.ServiceId,
Plugins: (*v1.Plugins)(&route.Plugins),
}, nil
}
78 changes: 78 additions & 0 deletions pkg/apisix/resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You 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 apisix

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
)

func TestItemUnmarshalJSON(t *testing.T) {
var items node
emptyData := `
{
"key": "test",
"nodes": {}
}
`
err := json.Unmarshal([]byte(emptyData), &items)
assert.Nil(t, err)

emptyData = `
{
"key": "test",
"nodes": {"a": "b", "c": "d"}
}
`
err = json.Unmarshal([]byte(emptyData), &items)
assert.Equal(t, err.Error(), "unexpected non-empty object")

emptyArray := `
{
"key": "test",
"nodes": []
}
`
err = json.Unmarshal([]byte(emptyArray), &items)
assert.Nil(t, err)
}

func TestItemConvertRoute(t *testing.T) {
item := &item{
Key: "/apisix/routes/001",
Value: json.RawMessage(`
{
"upstream_id": "13",
"service_id": "14",
"host": "foo.com",
"uri": "/shop/133/details",
"methods": ["GET", "POST"]
}
`),
}

r, err := item.route("qa")
assert.Nil(t, err)
assert.Equal(t, *r.UpstreamId, "13")
assert.Equal(t, *r.ServiceId, "14")
assert.Equal(t, *r.Host, "foo.com")
assert.Equal(t, *r.Path, "/shop/133/details")
assert.Equal(t, *r.Methods[0], "GET")
assert.Equal(t, *r.Methods[1], "POST")
assert.Nil(t, r.Name)
assert.Equal(t, *r.FullName, "qa_unknown")
}
Loading

0 comments on commit a6d92e9

Please sign in to comment.