Compare commits

...

38 Commits

Author SHA1 Message Date
Bingkun Zhao
10f5267b3f http2rpc supports treating the entire body as a method parameter (#722) 2023-12-21 14:47:08 +08:00
澄潭
cec99686a0 Update Makefile.core.mk 2023-12-21 11:34:53 +08:00
澄潭
2d5d9c095b rel: Release version 1.3.2 (#719) 2023-12-20 20:00:10 +08:00
澄潭
4bd4433248 fix eureka service discovery not work in standalone mode (#714) 2023-12-20 19:05:37 +08:00
澄潭
4ea85e9a35 support empty config with custom config func (#718) 2023-12-20 19:05:04 +08:00
澄潭
a140f780d2 ignore binary body in plugins (#711) 2023-12-19 16:47:15 +08:00
Se7en
2548815667 Compatible with nginx.ingress.kubernetes.io/canary-by-header-pattern annotation (#693) 2023-12-19 15:42:26 +08:00
Bingkun Zhao
e760b4d0ab optimize http2dubbo filter of envoy (#704) 2023-12-19 09:57:58 +08:00
Jun
3cc1c7877f feat: add gzip global setting in configmap (#660) 2023-12-18 19:05:29 +08:00
澄潭
8039b82699 Update README.md 2023-12-18 09:53:24 +08:00
SJC
f9a015e45a bug: shorthand l repeated (#702)
Signed-off-by: sjcsjc123 <1401189096@qq.com>
2023-12-18 09:46:24 +08:00
船长
5fbfbe0e4a Change jwt-auth plugin name to simple-jwt-auth (#698) 2023-12-15 13:39:49 +08:00
澄潭
a3339a9b1c Revert "feat: add windows build" (#692) 2023-12-14 17:24:07 +08:00
fsl
aa94412af2 feat: add windows build (#691)
Signed-off-by: fengshunli <1171313930@qq.com>
2023-12-14 14:40:28 +08:00
澄潭
817925ef39 fix gateway name (#672) 2023-12-14 11:04:55 +08:00
SJC
c55a5b9bd9 opt: hgctl dashboard/completion optimize (#677)
Signed-off-by: sjcsjc123 <1401189096@qq.com>
2023-12-13 15:16:39 +08:00
Uncle-Justice
518d8dfa3d refactor: unify image customization methods (#687) 2023-12-12 19:13:45 +08:00
dongjiang
d2ee6065a0 feat: add key-auth plugin (#586)
Signed-off-by: dongjiang1989 <dongjiang1989@126.com>
2023-12-11 10:03:52 +08:00
Hinsteny Hisoka
4426f18a84 Add test cases for http2rpc (#662) 2023-12-09 18:05:38 +08:00
Kent Dong
17794cef2a fix: Remove -p/--console-password parameters from get-higress.sh (#675) 2023-12-08 11:56:27 +08:00
SJC
a554ee1ceb opt(hgctl/dashboard): avoid printing error messages cannot open browser (#665)
Signed-off-by: sjcsjc123 <1401189096@qq.com>
2023-12-06 12:02:35 +08:00
澄潭
1dbb130539 Update build-and-test-plugin.yaml 2023-12-06 10:58:02 +08:00
澄潭
9c1684c941 Update build-and-test-plugin.yaml 2023-12-05 23:07:37 +08:00
Jun
bd4109e1a4 feat: store profile to configmap or home dir and merge profiles to select when upgrade and uninstall (#649) 2023-12-05 15:59:36 +08:00
澄潭
967fa3f3d1 optimize ci (#659) 2023-12-01 16:11:13 +08:00
Hinsteny Hisoka
d57ffce1dc Fix bug for when Http2Rpc been delete or addupdate need to push xds server (#657) 2023-12-01 14:13:13 +08:00
liushp
a2d97ae98f fix x-ca-timestamp validate (#653) 2023-11-27 11:21:11 +08:00
澄潭
324e0bcf91 optimize rds (#655) 2023-11-24 14:32:51 +08:00
johnlanni
14742705b1 add timeout&local-rate-limt annotations 2023-11-16 17:33:03 +08:00
澄潭
b204ad4c8d rel: Release version 1.3.1 (#640) 2023-11-16 11:31:37 +08:00
澄潭
34054f8c76 optimize xds push (#638) 2023-11-16 09:32:58 +08:00
澄潭
6803aa44ab add new helm value for cds push (#639) 2023-11-16 09:31:25 +08:00
澄潭
e5cd334d5d support timeout and ratelimit (#637) 2023-11-15 20:43:49 +08:00
澄潭
88c0386ca3 more compatiable with nginx's rewrite (#636) 2023-11-14 18:59:13 +08:00
澄潭
5174397e7c Sync inner fix (#634) 2023-11-14 11:15:26 +08:00
澄潭
cb0479510f add oauth2 plugin (#632) 2023-11-13 11:03:46 +08:00
Jun
57b8cb1d69 fix: fix hgctl in high kubenetes version problem by auto detecting k8s version and adding helm lookup function (#629) 2023-11-08 20:55:54 +08:00
澄潭
9f5b795a4d wasm: strip port from host when match host (#626) 2023-11-07 17:11:56 +08:00
115 changed files with 8089 additions and 436 deletions

View File

@@ -0,0 +1,70 @@
name: "Build and Test Plugins"
on:
push:
branches: [ main ]
paths:
- 'plugins/**'
- 'test/**'
pull_request:
branches: ["*"]
paths:
- 'plugins/**'
- 'test/**'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: 1.19
# There are too many lint errors in current code bases
# uncomment when we decide what lint should be addressed or ignored.
# - run: make lint
higress-wasmplugin-test:
runs-on: ubuntu-latest
strategy:
matrix:
# TODO(Xunzhuo): Enable C WASM Filters in CI
wasmPluginType: [ GO ]
steps:
- uses: actions/checkout@v3
- name: "Setup Go"
uses: actions/setup-go@v3
with:
go-version: 1.19
- name: Setup Golang Caches
uses: actions/cache@v3
with:
path: |-
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-go
- name: Setup Submodule Caches
uses: actions/cache@v3
with:
path: |-
envoy
istio
.git/modules
key: ${{ runner.os }}-submodules-new-${{ github.run_id }}
restore-keys: ${{ runner.os }}-submodules-new
- run: git stash # restore patch
- name: "Run Ingress WasmPlugins Tests"
run: GOPROXY="https://proxy.golang.org,direct" PLUGIN_TYPE=${{ matrix.wasmPluginType }} make higress-wasmplugin-test
publish:
runs-on: ubuntu-latest
needs: [higress-wasmplugin-test]
steps:
- uses: actions/checkout@v3

View File

@@ -141,48 +141,8 @@ jobs:
- name: "Run Higress E2E Conformance Tests"
run: GOPROXY="https://proxy.golang.org,direct" make higress-conformance-test
higress-wasmplugin-test:
runs-on: ubuntu-latest
needs: [build]
strategy:
matrix:
# TODO(Xunzhuo): Enable C WASM Filters in CI
wasmPluginType: [ GO ]
steps:
- uses: actions/checkout@v3
- name: "Setup Go"
uses: actions/setup-go@v3
with:
go-version: 1.19
- name: Setup Golang Caches
uses: actions/cache@v3
with:
path: |-
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-go
- name: Setup Submodule Caches
uses: actions/cache@v3
with:
path: |-
envoy
istio
.git/modules
key: ${{ runner.os }}-submodules-new-${{ github.run_id }}
restore-keys: ${{ runner.os }}-submodules-new
- run: git stash # restore patch
- name: "Run Ingress WasmPlugins Tests"
run: GOPROXY="https://proxy.golang.org,direct" PLUGIN_TYPE=${{ matrix.wasmPluginType }} make higress-wasmplugin-test
publish:
runs-on: ubuntu-latest
needs: [higress-conformance-test,gateway-conformance-test,higress-wasmplugin-test]
needs: [higress-conformance-test,gateway-conformance-test]
steps:
- uses: actions/checkout@v3

View File

@@ -137,11 +137,11 @@ export ENVOY_TAR_PATH:=/home/package/envoy.tar.gz
external/package/envoy-amd64.tar.gz:
# cd external/proxy; BUILD_WITH_CONTAINER=1 make test_release
cd external/package; wget "https://github.com/alibaba/higress/releases/download/v1.2.0/envoy-amd64.tar.gz"
cd external/package; wget "https://github.com/alibaba/higress/releases/download/v1.3.0/envoy-amd64.tar.gz"
external/package/envoy-arm64.tar.gz:
# cd external/proxy; BUILD_WITH_CONTAINER=1 make test_release
cd external/package; wget "https://github.com/alibaba/higress/releases/download/v1.2.0/envoy-arm64.tar.gz"
cd external/package; wget "https://github.com/alibaba/higress/releases/download/v1.3.0/envoy-arm64.tar.gz"
build-pilot:
cd external/istio; rm -rf out/linux_amd64; GOOS_LOCAL=linux TARGET_OS=linux TARGET_ARCH=amd64 BUILD_WITH_CONTAINER=1 make build-linux
@@ -176,8 +176,8 @@ install: pre-install
cd helm/higress; helm dependency build
helm install higress helm/higress -n higress-system --create-namespace --set 'global.local=true'
ENVOY_LATEST_IMAGE_TAG ?= sha-6835486
ISTIO_LATEST_IMAGE_TAG ?= sha-6835486
ENVOY_LATEST_IMAGE_TAG ?= sha-2d5d9c0
ISTIO_LATEST_IMAGE_TAG ?= sha-2d5d9c0
install-dev: pre-install
helm install higress helm/core -n higress-system --create-namespace --set 'controller.tag=$(TAG)' --set 'gateway.replicas=1' --set 'pilot.tag=$(ISTIO_LATEST_IMAGE_TAG)' --set 'gateway.tag=$(ENVOY_LATEST_IMAGE_TAG)' --set 'global.local=true'
@@ -257,13 +257,13 @@ delete-cluster: $(tools/kind) ## Delete kind cluster.
.PHONY: kube-load-image
kube-load-image: $(tools/kind) ## Install the Higress image to a kind cluster using the provided $IMAGE and $TAG.
tools/hack/kind-load-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/higress $(TAG)
tools/hack/docker-pull-image.sh docker.io/alihigress/dubbo-provider-demo 0.0.1
tools/hack/docker-pull-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/dubbo-provider-demo 0.0.3-x86
tools/hack/docker-pull-image.sh docker.io/alihigress/nacos-standlone-rc3 1.0.0-RC3
tools/hack/docker-pull-image.sh docker.io/hashicorp/consul 1.16.0
tools/hack/docker-pull-image.sh docker.io/charlie1380/eureka-registry-provider v0.3.0
tools/hack/docker-pull-image.sh docker.io/bitinit/eureka latest
tools/hack/docker-pull-image.sh docker.io/alihigress/httpbin 1.0.2
tools/hack/kind-load-image.sh docker.io/alihigress/dubbo-provider-demo 0.0.1
tools/hack/kind-load-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/dubbo-provider-demo 0.0.3-x86
tools/hack/kind-load-image.sh docker.io/alihigress/nacos-standlone-rc3 1.0.0-RC3
tools/hack/kind-load-image.sh docker.io/hashicorp/consul 1.16.0
tools/hack/kind-load-image.sh docker.io/alihigress/httpbin 1.0.2

View File

@@ -121,13 +121,7 @@ Higress 是基于阿里内部两年多的 Envoy Gateway 实践沉淀,以开源
### 联系我们
- Mailing list: higress@googlegroups.com
社区交流群:
![image](https://img.alicdn.com/imgextra/i1/O1CN01KWonlE1DkpqaYVTiC_!!6000000000255-0-tps-720-405.jpg)
![image](https://img.alicdn.com/imgextra/i2/O1CN01qPd7Ix1uZPVEsWjWp_!!6000000006051-0-tps-720-405.jpg)
开发者群:
![image](https://img.alicdn.com/imgextra/i2/O1CN010jFMgn1qTDaHqeIgH_!!6000000005496-2-tps-406-531.png)

View File

@@ -1 +1 @@
v1.3.0
v1.3.2

View File

@@ -154,6 +154,11 @@ spec:
type: array
httpPath:
type: string
paramFromEntireBody:
properties:
paramType:
type: string
type: object
params:
items:
properties:

View File

@@ -200,14 +200,15 @@ func (m *DubboService) GetMethods() []*Method {
}
type Method struct {
ServiceMethod string `protobuf:"bytes,1,opt,name=service_method,json=serviceMethod,proto3" json:"service_method,omitempty"`
HeadersAttach string `protobuf:"bytes,2,opt,name=headers_attach,json=headersAttach,proto3" json:"headers_attach,omitempty"`
HttpPath string `protobuf:"bytes,3,opt,name=http_path,json=httpPath,proto3" json:"http_path,omitempty"`
HttpMethods []string `protobuf:"bytes,4,rep,name=http_methods,json=httpMethods,proto3" json:"http_methods,omitempty"`
Params []*Param `protobuf:"bytes,5,rep,name=params,proto3" json:"params,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
ServiceMethod string `protobuf:"bytes,1,opt,name=service_method,json=serviceMethod,proto3" json:"service_method,omitempty"`
HeadersAttach string `protobuf:"bytes,2,opt,name=headers_attach,json=headersAttach,proto3" json:"headers_attach,omitempty"`
HttpPath string `protobuf:"bytes,3,opt,name=http_path,json=httpPath,proto3" json:"http_path,omitempty"`
HttpMethods []string `protobuf:"bytes,4,rep,name=http_methods,json=httpMethods,proto3" json:"http_methods,omitempty"`
Params []*Param `protobuf:"bytes,5,rep,name=params,proto3" json:"params,omitempty"`
ParamFromEntireBody *ParamFromEntireBody `protobuf:"bytes,6,opt,name=paramFromEntireBody,proto3" json:"paramFromEntireBody,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Method) Reset() { *m = Method{} }
@@ -278,6 +279,13 @@ func (m *Method) GetParams() []*Param {
return nil
}
func (m *Method) GetParamFromEntireBody() *ParamFromEntireBody {
if m != nil {
return m.ParamFromEntireBody
}
return nil
}
type Param struct {
ParamSource string `protobuf:"bytes,1,opt,name=param_source,json=paramSource,proto3" json:"param_source,omitempty"`
ParamKey string `protobuf:"bytes,2,opt,name=param_key,json=paramKey,proto3" json:"param_key,omitempty"`
@@ -341,6 +349,53 @@ func (m *Param) GetParamType() string {
return ""
}
type ParamFromEntireBody struct {
ParamType string `protobuf:"bytes,1,opt,name=param_type,json=paramType,proto3" json:"param_type,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *ParamFromEntireBody) Reset() { *m = ParamFromEntireBody{} }
func (m *ParamFromEntireBody) String() string { return proto.CompactTextString(m) }
func (*ParamFromEntireBody) ProtoMessage() {}
func (*ParamFromEntireBody) Descriptor() ([]byte, []int) {
return fileDescriptor_dc706c3b890c1c84, []int{4}
}
func (m *ParamFromEntireBody) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
}
func (m *ParamFromEntireBody) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
if deterministic {
return xxx_messageInfo_ParamFromEntireBody.Marshal(b, m, deterministic)
} else {
b = b[:cap(b)]
n, err := m.MarshalToSizedBuffer(b)
if err != nil {
return nil, err
}
return b[:n], nil
}
}
func (m *ParamFromEntireBody) XXX_Merge(src proto.Message) {
xxx_messageInfo_ParamFromEntireBody.Merge(m, src)
}
func (m *ParamFromEntireBody) XXX_Size() int {
return m.Size()
}
func (m *ParamFromEntireBody) XXX_DiscardUnknown() {
xxx_messageInfo_ParamFromEntireBody.DiscardUnknown(m)
}
var xxx_messageInfo_ParamFromEntireBody proto.InternalMessageInfo
func (m *ParamFromEntireBody) GetParamType() string {
if m != nil {
return m.ParamType
}
return ""
}
type GrpcService struct {
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
@@ -351,7 +406,7 @@ func (m *GrpcService) Reset() { *m = GrpcService{} }
func (m *GrpcService) String() string { return proto.CompactTextString(m) }
func (*GrpcService) ProtoMessage() {}
func (*GrpcService) Descriptor() ([]byte, []int) {
return fileDescriptor_dc706c3b890c1c84, []int{4}
return fileDescriptor_dc706c3b890c1c84, []int{5}
}
func (m *GrpcService) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
@@ -385,42 +440,46 @@ func init() {
proto.RegisterType((*DubboService)(nil), "higress.networking.v1.DubboService")
proto.RegisterType((*Method)(nil), "higress.networking.v1.Method")
proto.RegisterType((*Param)(nil), "higress.networking.v1.Param")
proto.RegisterType((*ParamFromEntireBody)(nil), "higress.networking.v1.ParamFromEntireBody")
proto.RegisterType((*GrpcService)(nil), "higress.networking.v1.GrpcService")
}
func init() { proto.RegisterFile("networking/v1/http_2_rpc.proto", fileDescriptor_dc706c3b890c1c84) }
var fileDescriptor_dc706c3b890c1c84 = []byte{
// 463 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x53, 0xcf, 0x8a, 0xd3, 0x40,
0x18, 0x77, 0xba, 0x6d, 0xb7, 0xfb, 0x65, 0xeb, 0x61, 0x40, 0x08, 0x8b, 0xc6, 0x35, 0x7b, 0x70,
0x41, 0x49, 0xd8, 0xea, 0x41, 0x14, 0x0f, 0x5b, 0x04, 0x17, 0x44, 0x58, 0xb2, 0x9e, 0xbc, 0x84,
0x49, 0x32, 0x66, 0x86, 0x6d, 0x33, 0xc3, 0xcc, 0x34, 0xda, 0xb7, 0xf0, 0x35, 0x7c, 0x13, 0x8f,
0x3e, 0x82, 0x14, 0x1f, 0x44, 0x32, 0x93, 0x6e, 0x13, 0xb1, 0xb7, 0xf0, 0xfb, 0x33, 0xdf, 0xef,
0xc7, 0xf7, 0x05, 0x82, 0x8a, 0x9a, 0xaf, 0x42, 0xdd, 0xf2, 0xaa, 0x8c, 0xeb, 0x8b, 0x98, 0x19,
0x23, 0xd3, 0x59, 0xaa, 0x64, 0x1e, 0x49, 0x25, 0x8c, 0xc0, 0x0f, 0x18, 0x2f, 0x15, 0xd5, 0x3a,
0xda, 0xe9, 0xa2, 0xfa, 0xe2, 0xe4, 0x71, 0x29, 0x44, 0xb9, 0xa0, 0x31, 0x91, 0x3c, 0xfe, 0xc2,
0xe9, 0xa2, 0x48, 0x33, 0xca, 0x48, 0xcd, 0x85, 0x72, 0xbe, 0xf0, 0x3b, 0x82, 0xc9, 0x95, 0x31,
0x72, 0x96, 0xc8, 0x1c, 0xbf, 0x81, 0x51, 0xb1, 0xca, 0x32, 0xe1, 0xa3, 0x53, 0x74, 0xee, 0xcd,
0xce, 0xa2, 0xff, 0x3e, 0x1a, 0xbd, 0x6b, 0x34, 0x37, 0x54, 0xd5, 0x3c, 0xa7, 0x57, 0xf7, 0x12,
0xe7, 0xc1, 0xaf, 0x60, 0x58, 0x2a, 0x99, 0xfb, 0x03, 0xeb, 0x0d, 0xf7, 0x78, 0xdf, 0x2b, 0x99,
0xef, 0xac, 0xd6, 0x31, 0x9f, 0x82, 0x57, 0x50, 0x6d, 0x78, 0x45, 0x0c, 0x17, 0x55, 0xf8, 0x03,
0xc1, 0x71, 0x77, 0x04, 0x0e, 0xe0, 0x50, 0xbb, 0x4f, 0x1b, 0xec, 0x68, 0x3e, 0xdc, 0x5c, 0xa2,
0x41, 0xb2, 0x05, 0x1b, 0xbe, 0xa6, 0x4a, 0x73, 0x51, 0xd9, 0xe1, 0x77, 0x7c, 0x0b, 0xe2, 0x13,
0x18, 0x95, 0x4a, 0xac, 0xa4, 0x7f, 0x70, 0xc7, 0xa2, 0xc4, 0x41, 0xf8, 0x2d, 0x1c, 0x2e, 0xa9,
0x61, 0xa2, 0xd0, 0xfe, 0xf0, 0xf4, 0xe0, 0xdc, 0x9b, 0x3d, 0xda, 0x13, 0xfc, 0xa3, 0x55, 0x6d,
0x9f, 0x6e, 0x3d, 0xe1, 0x1f, 0x04, 0x63, 0xc7, 0xe0, 0x67, 0x70, 0xbf, 0x0d, 0x94, 0x3a, 0xb6,
0x17, 0x76, 0xda, 0x72, 0x3b, 0x31, 0xa3, 0xa4, 0xa0, 0x4a, 0xa7, 0xc4, 0x18, 0x92, 0xb3, 0x4e,
0x72, 0x94, 0x4c, 0x5b, 0xee, 0xd2, 0x52, 0xf8, 0x09, 0x1c, 0xd9, 0x7d, 0x4b, 0x62, 0x58, 0xa7,
0xc3, 0x20, 0x99, 0x34, 0xf0, 0x35, 0x31, 0x0c, 0x3f, 0x85, 0x63, 0x2b, 0xe9, 0x76, 0xd9, 0xaa,
0xbc, 0x86, 0x71, 0x73, 0x35, 0x7e, 0x09, 0x63, 0x49, 0x14, 0x59, 0x6a, 0x7f, 0x64, 0xeb, 0x3e,
0xdc, 0x53, 0xf7, 0xba, 0x11, 0x25, 0xad, 0x36, 0xfc, 0x06, 0x23, 0x0b, 0x34, 0x73, 0x2c, 0x94,
0x6a, 0xb1, 0x52, 0xff, 0xec, 0xc3, 0xb3, 0xcc, 0x8d, 0x25, 0x9a, 0xcc, 0x4e, 0x78, 0x4b, 0xd7,
0xbd, 0xad, 0x4c, 0x2c, 0xfc, 0x81, 0xae, 0xf1, 0x19, 0x80, 0x93, 0x98, 0xb5, 0xa4, 0xbd, 0x5e,
0xce, 0xfa, 0x69, 0x2d, 0x69, 0x38, 0x05, 0xaf, 0x73, 0x32, 0xf3, 0xd7, 0x3f, 0x37, 0x01, 0xfa,
0xb5, 0x09, 0xd0, 0xef, 0x4d, 0x80, 0x3e, 0x3f, 0x2f, 0xb9, 0x61, 0xab, 0x2c, 0xca, 0xc5, 0x32,
0x26, 0x0b, 0x9e, 0x91, 0x8c, 0xc4, 0x6d, 0x1d, 0x7b, 0xf1, 0xbd, 0x7f, 0x26, 0x1b, 0xdb, 0x8b,
0x7f, 0xf1, 0x37, 0x00, 0x00, 0xff, 0xff, 0x75, 0x5c, 0x9e, 0x28, 0x4b, 0x03, 0x00, 0x00,
// 506 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x93, 0xdf, 0x6e, 0xd3, 0x30,
0x14, 0xc6, 0x71, 0xd7, 0x76, 0xdd, 0xc9, 0xca, 0x85, 0x27, 0xa4, 0x68, 0x82, 0x32, 0xb2, 0x0b,
0x26, 0x40, 0x89, 0x56, 0xb8, 0x40, 0x43, 0x5c, 0xac, 0xe2, 0xcf, 0x24, 0x84, 0x34, 0x65, 0x5c,
0x71, 0x13, 0x39, 0x89, 0x49, 0xac, 0xb5, 0xb1, 0x65, 0xbb, 0x85, 0xbc, 0x05, 0xaf, 0xc1, 0x9b,
0xec, 0x92, 0x47, 0x40, 0x7d, 0x12, 0x14, 0x3b, 0x5d, 0x93, 0xa9, 0xdd, 0x5d, 0x74, 0xbe, 0xef,
0x77, 0x7c, 0x3e, 0x9f, 0x18, 0x46, 0x05, 0xd5, 0x3f, 0xb9, 0xbc, 0x66, 0x45, 0x16, 0x2c, 0x4e,
0x83, 0x5c, 0x6b, 0x11, 0x8d, 0x23, 0x29, 0x12, 0x5f, 0x48, 0xae, 0x39, 0x7e, 0x94, 0xb3, 0x4c,
0x52, 0xa5, 0xfc, 0xb5, 0xcf, 0x5f, 0x9c, 0x1e, 0x3e, 0xcd, 0x38, 0xcf, 0xa6, 0x34, 0x20, 0x82,
0x05, 0x3f, 0x18, 0x9d, 0xa6, 0x51, 0x4c, 0x73, 0xb2, 0x60, 0x5c, 0x5a, 0xce, 0xfb, 0x8d, 0x60,
0x70, 0xa1, 0xb5, 0x18, 0x87, 0x22, 0xc1, 0xef, 0xa0, 0x97, 0xce, 0xe3, 0x98, 0xbb, 0xe8, 0x08,
0x9d, 0x38, 0xe3, 0x63, 0x7f, 0x63, 0x53, 0xff, 0x43, 0xe5, 0xb9, 0xa2, 0x72, 0xc1, 0x12, 0x7a,
0xf1, 0x20, 0xb4, 0x0c, 0x7e, 0x0b, 0xdd, 0x4c, 0x8a, 0xc4, 0xed, 0x18, 0xd6, 0xdb, 0xc2, 0x7e,
0x96, 0x22, 0x59, 0xa3, 0x86, 0x98, 0x0c, 0xc1, 0x49, 0xa9, 0xd2, 0xac, 0x20, 0x9a, 0xf1, 0xc2,
0xfb, 0x83, 0x60, 0xbf, 0x79, 0x04, 0x1e, 0xc1, 0xae, 0xb2, 0x9f, 0x66, 0xb0, 0xbd, 0x49, 0x77,
0x79, 0x8e, 0x3a, 0xe1, 0xaa, 0x58, 0xe9, 0x0b, 0x2a, 0x15, 0xe3, 0x85, 0x39, 0xfc, 0x56, 0xaf,
0x8b, 0xf8, 0x10, 0x7a, 0x99, 0xe4, 0x73, 0xe1, 0xee, 0xdc, 0xaa, 0x28, 0xb4, 0x25, 0xfc, 0x1e,
0x76, 0x67, 0x54, 0xe7, 0x3c, 0x55, 0x6e, 0xf7, 0x68, 0xe7, 0xc4, 0x19, 0x3f, 0xd9, 0x32, 0xf8,
0x57, 0xe3, 0x5a, 0xb5, 0xae, 0x19, 0xef, 0xa6, 0x03, 0x7d, 0xab, 0xe0, 0x97, 0xf0, 0xb0, 0x1e,
0x28, 0xb2, 0x6a, 0x6b, 0xd8, 0x61, 0xad, 0xad, 0xcd, 0x39, 0x25, 0x29, 0x95, 0x2a, 0x22, 0x5a,
0x93, 0x24, 0x6f, 0x4c, 0x8e, 0xc2, 0x61, 0xad, 0x9d, 0x1b, 0x09, 0x3f, 0x83, 0x3d, 0xb3, 0x6f,
0x41, 0x74, 0xde, 0xc8, 0xd0, 0x09, 0x07, 0x55, 0xf9, 0x92, 0xe8, 0x1c, 0x3f, 0x87, 0x7d, 0x63,
0x69, 0x66, 0x59, 0xb9, 0x9c, 0x4a, 0xb1, 0xe7, 0x2a, 0xfc, 0x06, 0xfa, 0x82, 0x48, 0x32, 0x53,
0x6e, 0xcf, 0xc4, 0x7d, 0xbc, 0x25, 0xee, 0x65, 0x65, 0x0a, 0x6b, 0x2f, 0x8e, 0xe1, 0xc0, 0x7c,
0x7d, 0x92, 0x7c, 0xf6, 0xb1, 0xd0, 0x4c, 0xd2, 0x09, 0x4f, 0x4b, 0xb7, 0x6f, 0x56, 0xfd, 0xe2,
0xbe, 0x16, 0x6d, 0xa2, 0xce, 0xb7, 0xa9, 0x99, 0xf7, 0x0b, 0x7a, 0x86, 0xa8, 0xb2, 0x18, 0x3d,
0x52, 0x7c, 0x2e, 0xef, 0xec, 0xdc, 0x31, 0xca, 0x95, 0x11, 0xaa, 0x7b, 0xb1, 0xc6, 0x6b, 0x5a,
0xb6, 0x36, 0x3f, 0x30, 0xe5, 0x2f, 0xb4, 0xc4, 0xc7, 0x00, 0xd6, 0xa2, 0x4b, 0x41, 0x5b, 0x77,
0x67, 0xd1, 0x6f, 0xa5, 0xa0, 0xde, 0x19, 0x1c, 0x6c, 0x98, 0xf5, 0x0e, 0x8b, 0x36, 0xb3, 0x43,
0x70, 0x1a, 0xbf, 0xf4, 0xe4, 0xec, 0x66, 0x39, 0x42, 0x7f, 0x97, 0x23, 0xf4, 0x6f, 0x39, 0x42,
0xdf, 0x5f, 0x65, 0x4c, 0xe7, 0xf3, 0xd8, 0x4f, 0xf8, 0x2c, 0x20, 0x53, 0x16, 0x93, 0x98, 0x04,
0xf5, 0x5d, 0x99, 0x17, 0xd9, 0x7a, 0xd3, 0x71, 0xdf, 0xbc, 0xc8, 0xd7, 0xff, 0x03, 0x00, 0x00,
0xff, 0xff, 0x30, 0xef, 0x3d, 0xa9, 0xeb, 0x03, 0x00, 0x00,
}
func (m *Http2Rpc) Marshal() (dAtA []byte, err error) {
@@ -587,6 +646,18 @@ func (m *Method) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i -= len(m.XXX_unrecognized)
copy(dAtA[i:], m.XXX_unrecognized)
}
if m.ParamFromEntireBody != nil {
{
size, err := m.ParamFromEntireBody.MarshalToSizedBuffer(dAtA[:i])
if err != nil {
return 0, err
}
i -= size
i = encodeVarintHttp_2Rpc(dAtA, i, uint64(size))
}
i--
dAtA[i] = 0x32
}
if len(m.Params) > 0 {
for iNdEx := len(m.Params) - 1; iNdEx >= 0; iNdEx-- {
{
@@ -682,6 +753,40 @@ func (m *Param) MarshalToSizedBuffer(dAtA []byte) (int, error) {
return len(dAtA) - i, nil
}
func (m *ParamFromEntireBody) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalToSizedBuffer(dAtA[:size])
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *ParamFromEntireBody) MarshalTo(dAtA []byte) (int, error) {
size := m.Size()
return m.MarshalToSizedBuffer(dAtA[:size])
}
func (m *ParamFromEntireBody) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i := len(dAtA)
_ = i
var l int
_ = l
if m.XXX_unrecognized != nil {
i -= len(m.XXX_unrecognized)
copy(dAtA[i:], m.XXX_unrecognized)
}
if len(m.ParamType) > 0 {
i -= len(m.ParamType)
copy(dAtA[i:], m.ParamType)
i = encodeVarintHttp_2Rpc(dAtA, i, uint64(len(m.ParamType)))
i--
dAtA[i] = 0xa
}
return len(dAtA) - i, nil
}
func (m *GrpcService) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
@@ -819,6 +924,10 @@ func (m *Method) Size() (n int) {
n += 1 + l + sovHttp_2Rpc(uint64(l))
}
}
if m.ParamFromEntireBody != nil {
l = m.ParamFromEntireBody.Size()
n += 1 + l + sovHttp_2Rpc(uint64(l))
}
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
}
@@ -849,6 +958,22 @@ func (m *Param) Size() (n int) {
return n
}
func (m *ParamFromEntireBody) Size() (n int) {
if m == nil {
return 0
}
var l int
_ = l
l = len(m.ParamType)
if l > 0 {
n += 1 + l + sovHttp_2Rpc(uint64(l))
}
if m.XXX_unrecognized != nil {
n += len(m.XXX_unrecognized)
}
return n
}
func (m *GrpcService) Size() (n int) {
if m == nil {
return 0
@@ -1360,6 +1485,42 @@ func (m *Method) Unmarshal(dAtA []byte) error {
return err
}
iNdEx = postIndex
case 6:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field ParamFromEntireBody", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowHttp_2Rpc
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthHttp_2Rpc
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthHttp_2Rpc
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.ParamFromEntireBody == nil {
m.ParamFromEntireBody = &ParamFromEntireBody{}
}
if err := m.ParamFromEntireBody.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipHttp_2Rpc(dAtA[iNdEx:])
@@ -1529,6 +1690,89 @@ func (m *Param) Unmarshal(dAtA []byte) error {
}
return nil
}
func (m *ParamFromEntireBody) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowHttp_2Rpc
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: ParamFromEntireBody: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: ParamFromEntireBody: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field ParamType", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowHttp_2Rpc
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthHttp_2Rpc
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthHttp_2Rpc
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.ParamType = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipHttp_2Rpc(dAtA[iNdEx:])
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
return ErrInvalidLengthHttp_2Rpc
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...)
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func (m *GrpcService) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0

View File

@@ -62,6 +62,7 @@ message Method {
string http_path = 3 [(google.api.field_behavior) = REQUIRED];
repeated string http_methods = 4 [(google.api.field_behavior) = REQUIRED];
repeated Param params = 5;
ParamFromEntireBody paramFromEntireBody = 6 [(google.api.field_behavior) = OPTIONAL];
}
message Param {
@@ -70,5 +71,9 @@ message Param {
string param_type = 3 [(google.api.field_behavior) = REQUIRED];
}
message ParamFromEntireBody {
string param_type = 1 [(google.api.field_behavior) = REQUIRED];
}
message GrpcService {
}

View File

@@ -99,6 +99,27 @@ func (in *Param) DeepCopyInterface() interface{} {
return in.DeepCopy()
}
// DeepCopyInto supports using ParamFromEntireBody within kubernetes types, where deepcopy-gen is used.
func (in *ParamFromEntireBody) DeepCopyInto(out *ParamFromEntireBody) {
p := proto.Clone(in).(*ParamFromEntireBody)
*out = *p
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ParamFromEntireBody. Required by controller-gen.
func (in *ParamFromEntireBody) DeepCopy() *ParamFromEntireBody {
if in == nil {
return nil
}
out := new(ParamFromEntireBody)
in.DeepCopyInto(out)
return out
}
// DeepCopyInterface is an autogenerated deepcopy function, copying the receiver, creating a new ParamFromEntireBody. Required by controller-gen.
func (in *ParamFromEntireBody) DeepCopyInterface() interface{} {
return in.DeepCopy()
}
// DeepCopyInto supports using GrpcService within kubernetes types, where deepcopy-gen is used.
func (in *GrpcService) DeepCopyInto(out *GrpcService) {
p := proto.Clone(in).(*GrpcService)

View File

@@ -61,6 +61,17 @@ func (this *Param) UnmarshalJSON(b []byte) error {
return Http_2RpcUnmarshaler.Unmarshal(bytes.NewReader(b), this)
}
// MarshalJSON is a custom marshaler for ParamFromEntireBody
func (this *ParamFromEntireBody) MarshalJSON() ([]byte, error) {
str, err := Http_2RpcMarshaler.MarshalToString(this)
return []byte(str), err
}
// UnmarshalJSON is a custom unmarshaler for ParamFromEntireBody
func (this *ParamFromEntireBody) UnmarshalJSON(b []byte) error {
return Http_2RpcUnmarshaler.Unmarshal(bytes.NewReader(b), this)
}
// MarshalJSON is a custom marshaler for GrpcService
func (this *GrpcService) MarshalJSON() ([]byte, error) {
str, err := Http_2RpcMarshaler.MarshalToString(this)

View File

@@ -0,0 +1,315 @@
diff -Naur envoy/envoy/router/rds.h envoy-new/envoy/router/rds.h
--- envoy/envoy/router/rds.h 2023-11-24 10:52:39.914235488 +0800
+++ envoy-new/envoy/router/rds.h 2023-11-24 10:47:36.293873127 +0800
@@ -51,12 +51,6 @@
virtual void onConfigUpdate() PURE;
/**
- * Validate if the route configuration can be applied to the context of the route config provider.
- */
- virtual void
- validateConfig(const envoy::config::route::v3::RouteConfiguration& config) const PURE;
-
- /**
* Callback used to request an update to the route configuration from the management server.
* @param for_domain supplies the domain name that virtual hosts must match on
* @param thread_local_dispatcher thread-local dispatcher
diff -Naur envoy/envoy/router/route_config_update_receiver.h envoy-new/envoy/router/route_config_update_receiver.h
--- envoy/envoy/router/route_config_update_receiver.h 2023-11-24 10:52:39.918235651 +0800
+++ envoy-new/envoy/router/route_config_update_receiver.h 2023-11-24 10:47:36.293873127 +0800
@@ -27,6 +27,7 @@
* @param rc supplies the RouteConfiguration.
* @param version_info supplies RouteConfiguration version.
* @return bool whether RouteConfiguration has been updated.
+ * @throw EnvoyException if the new config can't be applied.
*/
virtual bool onRdsUpdate(const envoy::config::route::v3::RouteConfiguration& rc,
const std::string& version_info) PURE;
diff -Naur envoy/source/common/router/rds_impl.cc envoy-new/source/common/router/rds_impl.cc
--- envoy/source/common/router/rds_impl.cc 2023-11-24 10:52:40.194246888 +0800
+++ envoy-new/source/common/router/rds_impl.cc 2023-11-24 10:47:36.293873127 +0800
@@ -122,9 +122,6 @@
throw EnvoyException(fmt::format("Unexpected RDS configuration (expecting {}): {}",
route_config_name_, route_config.name()));
}
- if (route_config_provider_opt_.has_value()) {
- route_config_provider_opt_.value()->validateConfig(route_config);
- }
std::unique_ptr<Init::ManagerImpl> noop_init_manager;
std::unique_ptr<Cleanup> resume_rds;
if (config_update_info_->onRdsUpdate(route_config, version_info)) {
@@ -292,12 +289,6 @@
}
}
-void RdsRouteConfigProviderImpl::validateConfig(
- const envoy::config::route::v3::RouteConfiguration& config) const {
- // TODO(lizan): consider cache the config here until onConfigUpdate.
- ConfigImpl validation_config(config, optional_http_filters_, factory_context_, validator_, false);
-}
-
// Schedules a VHDS request on the main thread and queues up the callback to use when the VHDS
// response has been propagated to the worker thread that was the request origin.
void RdsRouteConfigProviderImpl::requestVirtualHostsUpdate(
diff -Naur envoy/source/common/router/rds_impl.h envoy-new/source/common/router/rds_impl.h
--- envoy/source/common/router/rds_impl.h 2023-11-24 10:52:40.194246888 +0800
+++ envoy-new/source/common/router/rds_impl.h 2023-11-24 10:47:36.293873127 +0800
@@ -81,7 +81,6 @@
}
SystemTime lastUpdated() const override { return last_updated_; }
void onConfigUpdate() override {}
- void validateConfig(const envoy::config::route::v3::RouteConfiguration&) const override {}
void requestVirtualHostsUpdate(const std::string&, Event::Dispatcher&,
std::weak_ptr<Http::RouteConfigUpdatedCallback>) override {
NOT_IMPLEMENTED_GCOVR_EXCL_LINE;
@@ -209,7 +208,6 @@
void requestVirtualHostsUpdate(
const std::string& for_domain, Event::Dispatcher& thread_local_dispatcher,
std::weak_ptr<Http::RouteConfigUpdatedCallback> route_config_updated_cb) override;
- void validateConfig(const envoy::config::route::v3::RouteConfiguration& config) const override;
private:
struct ThreadLocalConfig : public ThreadLocal::ThreadLocalObject {
diff -Naur envoy/source/common/router/route_config_update_receiver_impl.cc envoy-new/source/common/router/route_config_update_receiver_impl.cc
--- envoy/source/common/router/route_config_update_receiver_impl.cc 2023-11-24 10:52:40.194246888 +0800
+++ envoy-new/source/common/router/route_config_update_receiver_impl.cc 2023-11-24 10:47:36.297873290 +0800
@@ -1,6 +1,7 @@
#include "source/common/router/route_config_update_receiver_impl.h"
#include <string>
+#include <utility>
#include "envoy/config/route/v3/route.pb.h"
#include "envoy/service/discovery/v3/discovery.pb.h"
@@ -14,23 +15,49 @@
namespace Envoy {
namespace Router {
+namespace {
+
+// Resets 'route_config::virtual_hosts' by merging VirtualHost contained in
+// 'rds_vhosts' and 'vhds_vhosts'.
+void rebuildRouteConfigVirtualHosts(
+ const std::map<std::string, envoy::config::route::v3::VirtualHost>& rds_vhosts,
+ const std::map<std::string, envoy::config::route::v3::VirtualHost>& vhds_vhosts,
+ envoy::config::route::v3::RouteConfiguration& route_config) {
+ route_config.clear_virtual_hosts();
+ for (const auto& vhost : rds_vhosts) {
+ route_config.mutable_virtual_hosts()->Add()->CopyFrom(vhost.second);
+ }
+ for (const auto& vhost : vhds_vhosts) {
+ route_config.mutable_virtual_hosts()->Add()->CopyFrom(vhost.second);
+ }
+}
+
+} // namespace
+
bool RouteConfigUpdateReceiverImpl::onRdsUpdate(
const envoy::config::route::v3::RouteConfiguration& rc, const std::string& version_info) {
const uint64_t new_hash = MessageUtil::hash(rc);
if (new_hash == last_config_hash_) {
return false;
}
- route_config_proto_ = std::make_unique<envoy::config::route::v3::RouteConfiguration>(rc);
- last_config_hash_ = new_hash;
const uint64_t new_vhds_config_hash = rc.has_vhds() ? MessageUtil::hash(rc.vhds()) : 0ul;
+ std::map<std::string, envoy::config::route::v3::VirtualHost> rds_virtual_hosts;
+ for (const auto& vhost : rc.virtual_hosts()) {
+ rds_virtual_hosts.emplace(vhost.name(), vhost);
+ }
+ envoy::config::route::v3::RouteConfiguration new_route_config = rc;
+ rebuildRouteConfigVirtualHosts(rds_virtual_hosts, *vhds_virtual_hosts_, new_route_config);
+ auto new_config = std::make_shared<ConfigImpl>(
+ new_route_config, optional_http_filters_, factory_context_,
+ factory_context_.messageValidationContext().dynamicValidationVisitor(), false);
+ // If the above validation/validation doesn't raise exception, update the
+ // other cached config entries.
+ config_ = new_config;
+ rds_virtual_hosts_ = std::move(rds_virtual_hosts);
+ last_config_hash_ = new_hash;
+ *route_config_proto_ = std::move(new_route_config);
vhds_configuration_changed_ = new_vhds_config_hash != last_vhds_config_hash_;
last_vhds_config_hash_ = new_vhds_config_hash;
- initializeRdsVhosts(*route_config_proto_);
-
- rebuildRouteConfig(rds_virtual_hosts_, *vhds_virtual_hosts_, *route_config_proto_);
- config_ = std::make_shared<ConfigImpl>(
- *route_config_proto_, optional_http_filters_, factory_context_,
- factory_context_.messageValidationContext().dynamicValidationVisitor(), false);
onUpdateCommon(version_info);
return true;
@@ -50,8 +77,8 @@
auto route_config_after_this_update =
std::make_unique<envoy::config::route::v3::RouteConfiguration>();
route_config_after_this_update->CopyFrom(*route_config_proto_);
- rebuildRouteConfig(rds_virtual_hosts_, *vhosts_after_this_update,
- *route_config_after_this_update);
+ rebuildRouteConfigVirtualHosts(rds_virtual_hosts_, *vhosts_after_this_update,
+ *route_config_after_this_update);
auto new_config = std::make_shared<ConfigImpl>(
*route_config_after_this_update, optional_http_filters_, factory_context_,
@@ -73,14 +100,6 @@
config_info_.emplace(RouteConfigProvider::ConfigInfo{*route_config_proto_, last_config_version_});
}
-void RouteConfigUpdateReceiverImpl::initializeRdsVhosts(
- const envoy::config::route::v3::RouteConfiguration& route_configuration) {
- rds_virtual_hosts_.clear();
- for (const auto& vhost : route_configuration.virtual_hosts()) {
- rds_virtual_hosts_.emplace(vhost.name(), vhost);
- }
-}
-
bool RouteConfigUpdateReceiverImpl::removeVhosts(
std::map<std::string, envoy::config::route::v3::VirtualHost>& vhosts,
const Protobuf::RepeatedPtrField<std::string>& removed_vhost_names) {
@@ -110,18 +129,5 @@
return vhosts_added;
}
-void RouteConfigUpdateReceiverImpl::rebuildRouteConfig(
- const std::map<std::string, envoy::config::route::v3::VirtualHost>& rds_vhosts,
- const std::map<std::string, envoy::config::route::v3::VirtualHost>& vhds_vhosts,
- envoy::config::route::v3::RouteConfiguration& route_config) {
- route_config.clear_virtual_hosts();
- for (const auto& vhost : rds_vhosts) {
- route_config.mutable_virtual_hosts()->Add()->CopyFrom(vhost.second);
- }
- for (const auto& vhost : vhds_vhosts) {
- route_config.mutable_virtual_hosts()->Add()->CopyFrom(vhost.second);
- }
-}
-
} // namespace Router
} // namespace Envoy
diff -Naur envoy/source/common/router/route_config_update_receiver_impl.h envoy-new/source/common/router/route_config_update_receiver_impl.h
--- envoy/source/common/router/route_config_update_receiver_impl.h 2023-11-24 10:52:40.194246888 +0800
+++ envoy-new/source/common/router/route_config_update_receiver_impl.h 2023-11-24 10:47:36.297873290 +0800
@@ -27,15 +27,10 @@
std::make_unique<std::map<std::string, envoy::config::route::v3::VirtualHost>>()),
vhds_configuration_changed_(true), optional_http_filters_(optional_http_filters) {}
- void initializeRdsVhosts(const envoy::config::route::v3::RouteConfiguration& route_configuration);
bool removeVhosts(std::map<std::string, envoy::config::route::v3::VirtualHost>& vhosts,
const Protobuf::RepeatedPtrField<std::string>& removed_vhost_names);
bool updateVhosts(std::map<std::string, envoy::config::route::v3::VirtualHost>& vhosts,
const VirtualHostRefVector& added_vhosts);
- void rebuildRouteConfig(
- const std::map<std::string, envoy::config::route::v3::VirtualHost>& rds_vhosts,
- const std::map<std::string, envoy::config::route::v3::VirtualHost>& vhds_vhosts,
- envoy::config::route::v3::RouteConfiguration& route_config);
bool onDemandFetchFailed(const envoy::service::discovery::v3::Resource& resource) const;
void onUpdateCommon(const std::string& version_info);
diff -Naur envoy/source/server/admin/admin.h envoy-new/source/server/admin/admin.h
--- envoy/source/server/admin/admin.h 2023-11-24 10:52:41.358294284 +0800
+++ envoy-new/source/server/admin/admin.h 2023-11-24 10:47:36.297873290 +0800
@@ -234,7 +234,6 @@
absl::optional<ConfigInfo> configInfo() const override { return {}; }
SystemTime lastUpdated() const override { return time_source_.systemTime(); }
void onConfigUpdate() override {}
- void validateConfig(const envoy::config::route::v3::RouteConfiguration&) const override {}
void requestVirtualHostsUpdate(const std::string&, Event::Dispatcher&,
std::weak_ptr<Http::RouteConfigUpdatedCallback>) override {
NOT_IMPLEMENTED_GCOVR_EXCL_LINE;
diff -Naur envoy/test/common/router/rds_impl_test.cc envoy-new/test/common/router/rds_impl_test.cc
--- envoy/test/common/router/rds_impl_test.cc 2023-11-24 10:52:40.714268062 +0800
+++ envoy-new/test/common/router/rds_impl_test.cc 2023-11-24 10:47:36.297873290 +0800
@@ -528,34 +528,66 @@
rds_callbacks_->onConfigUpdate(decoded_resources.refvec_, response1.version_info());
}
-// Validate behavior when the config is delivered but it fails PGV validation.
+// Validates behavior when the config is delivered but it fails PGV validation.
+// The invalid config won't affect existing valid config.
TEST_F(RdsImplTest, FailureInvalidConfig) {
InSequence s;
setup();
+ EXPECT_CALL(init_watcher_, ready());
- const std::string response1_json = R"EOF(
+ const std::string valid_json = R"EOF(
{
"version_info": "1",
"resources": [
{
"@type": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
- "name": "INVALID_NAME_FOR_route_config",
+ "name": "foo_route_config",
"virtual_hosts": null
}
]
}
)EOF";
+
auto response1 =
- TestUtility::parseYaml<envoy::service::discovery::v3::DiscoveryResponse>(response1_json);
+ TestUtility::parseYaml<envoy::service::discovery::v3::DiscoveryResponse>(valid_json);
const auto decoded_resources =
TestUtility::decodeResources<envoy::config::route::v3::RouteConfiguration>(response1);
+ EXPECT_NO_THROW(
+ rds_callbacks_->onConfigUpdate(decoded_resources.refvec_, response1.version_info()));
+ // Sadly the RdsRouteConfigSubscription privately inherited from
+ // SubscriptionCallbacks, so we has to use reinterpret_cast here.
+ RdsRouteConfigSubscription* rds_subscription =
+ reinterpret_cast<RdsRouteConfigSubscription*>(rds_callbacks_);
+ auto config_impl_pointer = rds_subscription->routeConfigProvider().value()->config();
+ // Now send an invalid config update.
+ const std::string invalid_json =
+ R"EOF(
+{
+ "version_info": "1",
+ "resources": [
+ {
+ "@type": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
+ "name": "INVALID_NAME_FOR_route_config",
+ "virtual_hosts": null
+ }
+ ]
+}
+)EOF";
+
+ auto response2 =
+ TestUtility::parseYaml<envoy::service::discovery::v3::DiscoveryResponse>(invalid_json);
+ const auto decoded_resources_2 =
+ TestUtility::decodeResources<envoy::config::route::v3::RouteConfiguration>(response2);
- EXPECT_CALL(init_watcher_, ready());
EXPECT_THROW_WITH_MESSAGE(
- rds_callbacks_->onConfigUpdate(decoded_resources.refvec_, response1.version_info()),
+ rds_callbacks_->onConfigUpdate(decoded_resources_2.refvec_, response2.version_info()),
EnvoyException,
- "Unexpected RDS configuration (expecting foo_route_config): INVALID_NAME_FOR_route_config");
+ "Unexpected RDS configuration (expecting foo_route_config): "
+ "INVALID_NAME_FOR_route_config");
+
+ // Verify that the config is still the old value.
+ ASSERT_EQ(config_impl_pointer, rds_subscription->routeConfigProvider().value()->config());
}
// rds and vhds configurations change together
diff -Naur envoy/test/mocks/router/mocks.h envoy-new/test/mocks/router/mocks.h
--- envoy/test/mocks/router/mocks.h 2023-11-24 10:52:41.370294773 +0800
+++ envoy-new/test/mocks/router/mocks.h 2023-11-24 10:47:36.301873453 +0800
@@ -538,7 +538,6 @@
MOCK_METHOD(absl::optional<ConfigInfo>, configInfo, (), (const));
MOCK_METHOD(SystemTime, lastUpdated, (), (const));
MOCK_METHOD(void, onConfigUpdate, ());
- MOCK_METHOD(void, validateConfig, (const envoy::config::route::v3::RouteConfiguration&), (const));
MOCK_METHOD(void, requestVirtualHostsUpdate,
(const std::string&, Event::Dispatcher&,
std::weak_ptr<Http::RouteConfigUpdatedCallback> route_config_updated_cb));
diff -Naur envoy/tools/spelling/spelling_dictionary.txt envoy-new/tools/spelling/spelling_dictionary.txt
--- envoy/tools/spelling/spelling_dictionary.txt 2023-11-24 10:52:41.370294773 +0800
+++ envoy-new/tools/spelling/spelling_dictionary.txt 2023-11-24 10:48:54.969076506 +0800
@@ -1303,6 +1303,7 @@
ep
suri
transid
+vhosts
WAF
TRI
tmd

View File

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: 1.3.0
appVersion: 1.3.2
description: Helm chart for deploying higress gateways
icon: https://higress.io/img/higress_logo_small.png
home: http://higress.io/
@@ -10,4 +10,4 @@ name: higress-core
sources:
- http://github.com/alibaba/higress
type: application
version: 1.3.0
version: 1.3.2

View File

@@ -31,11 +31,7 @@ spec:
containers:
{{- if not .Values.global.enableHigressIstio }}
- name: discovery
{{- if contains "/" .Values.pilot.image }}
image: "{{ .Values.pilot.image }}"
{{- else }}
image: "{{ .Values.pilot.hub | default .Values.global.hub }}/{{ .Values.pilot.image | default "pilot" }}:{{ .Values.pilot.tag | default .Chart.AppVersion }}"
{{- end }}
{{- if .Values.global.imagePullPolicy }}
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
{{- end }}
@@ -75,7 +71,7 @@ spec:
timeoutSeconds: 5
env:
- name: PILOT_FILTER_GATEWAY_CLUSTER_CONFIG
value: "true"
value: "{{ .Values.global.onlyPushRouteCluster }}"
- name: HIGRESS_CONTROLLER_SVC
value: "127.0.0.1"
- name: HIGRESS_CONTROLLER_PORT
@@ -184,7 +180,7 @@ spec:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.controller.securityContext | nindent 12 }}
image: "{{ .Values.hub }}/{{ .Values.controller.image }}:{{ .Values.controller.tag | default .Chart.AppVersion }}"
image: "{{ .Values.controller.hub | default .Values.global.hub }}/{{ .Values.controller.image | default "higress" }}:{{ .Values.controller.tag | default .Chart.AppVersion }}"
args:
- "serve"
- --gatewaySelectorKey=higress

View File

@@ -68,7 +68,7 @@ spec:
{{- end }}
containers:
- name: higress-gateway
image: "{{ .Values.hub }}/{{ .Values.gateway.image }}:{{ .Values.gateway.tag | default .Chart.AppVersion }}"
image: "{{ .Values.gateway.hub | default .Values.global.hub }}/{{ .Values.gateway.image | default "gateway" }}:{{ .Values.gateway.tag | default .Chart.AppVersion }}"
args:
- proxy
- router
@@ -134,6 +134,8 @@ spec:
valueFrom:
fieldRef:
fieldPath: spec.serviceAccountName
- name: PILOT_XDS_SEND_TIMEOUT
value: 60s
- name: PROXY_XDS_VIA_AGENT
value: "true"
- name: ENABLE_INGRESS_GATEWAY_SDS

View File

@@ -1,5 +1,6 @@
revision: ""
global:
onlyPushRouteCluster: true
# IngressClass filters which ingress resources the higress controller watches.
# The default ingress class is higress.
# There are some special cases for special ingress class.
@@ -368,6 +369,8 @@ gateway:
name: "higress-gateway"
replicas: 2
image: gateway
hub: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress
tag: ""
# revision declares which revision this gateway is a part of
revision: ""
@@ -456,6 +459,8 @@ controller:
name: "higress-controller"
replicas: 1
image: higress
hub: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress
tag: ""
env: {}

View File

@@ -1,9 +1,9 @@
dependencies:
- name: higress-core
repository: file://../core
version: 1.3.0
version: 1.3.2
- name: higress-console
repository: https://higress.io/helm-charts/
version: 1.3.0
digest: sha256:3efc59ad8cd92ab4c3c87abeed8e2fc0288bb3ecc2805888ba6eaaf265ba6a10
generated: "2023-11-02T11:45:56.011629+08:00"
version: 1.3.1
digest: sha256:cf9b5f572f8e47348b3081a5620ad0165b400e4823a4ed36bd0597f3c794cbf3
generated: "2023-12-20T19:57:57.037118+08:00"

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: 1.3.0
appVersion: 1.3.2
description: Helm chart for deploying Higress gateways
icon: https://higress.io/img/higress_logo_small.png
home: http://higress.io/
@@ -12,9 +12,9 @@ sources:
dependencies:
- name: higress-core
repository: "file://../core"
version: 1.3.0
version: 1.3.2
- name: higress-console
repository: "https://higress.io/helm-charts/"
version: 1.3.0
version: 1.3.1
type: application
version: 1.3.0
version: 1.3.2

View File

@@ -0,0 +1,62 @@
diff -Naur istio/pilot/pkg/xds/ads.go istio-new/pilot/pkg/xds/ads.go
--- istio/pilot/pkg/xds/ads.go 2023-11-15 20:25:18.000000000 +0800
+++ istio-new/pilot/pkg/xds/ads.go 2023-11-15 20:24:20.000000000 +0800
@@ -318,6 +318,27 @@
<-con.initialized
for {
+ // Go select{} statements are not ordered; the same channel can be chosen many times.
+ // For requests, these are higher priority (client may be blocked on startup until these are done)
+ // and often very cheap to handle (simple ACK), so we check it first.
+ select {
+ case req, ok := <-con.reqChan:
+ if ok {
+ if err := s.processRequest(req, con); err != nil {
+ return err
+ }
+ } else {
+ // Remote side closed connection or error processing the request.
+ return <-con.errorChan
+ }
+ case <-con.stop:
+ return nil
+ default:
+ }
+ // If there wasn't already a request, poll for requests and pushes. Note: if we have a huge
+ // amount of incoming requests, we may still send some pushes, as we do not `continue` above;
+ // however, requests will be handled ~2x as much as pushes. This ensures a wave of requests
+ // cannot completely starve pushes. However, this scenario is unlikely.
select {
case req, ok := <-con.reqChan:
if ok {
diff -Naur istio/pilot/pkg/xds/delta.go istio-new/pilot/pkg/xds/delta.go
--- istio/pilot/pkg/xds/delta.go 2023-11-15 20:25:18.000000000 +0800
+++ istio-new/pilot/pkg/xds/delta.go 2023-11-15 20:24:44.000000000 +0800
@@ -102,6 +102,27 @@
<-con.initialized
for {
+ // Go select{} statements are not ordered; the same channel can be chosen many times.
+ // For requests, these are higher priority (client may be blocked on startup until these are done)
+ // and often very cheap to handle (simple ACK), so we check it first.
+ select {
+ case req, ok := <-con.deltaReqChan:
+ if ok {
+ if err := s.processDeltaRequest(req, con); err != nil {
+ return err
+ }
+ } else {
+ // Remote side closed connection or error processing the request.
+ return <-con.errorChan
+ }
+ case <-con.stop:
+ return nil
+ default:
+ }
+ // If there wasn't already a request, poll for requests and pushes. Note: if we have a huge
+ // amount of incoming requests, we may still send some pushes, as we do not `continue` above;
+ // however, requests will be handled ~2x as much as pushes. This ensures a wave of requests
+ // cannot completely starve pushes. However, this scenario is unlikely.
select {
case req, ok := <-con.deltaReqChan:
if ok {

232
pkg/cmd/hgctl/completion.go Normal file
View File

@@ -0,0 +1,232 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// 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 hgctl
import (
"fmt"
"io"
"os"
"path/filepath"
"github.com/spf13/cobra"
)
const completionDesc = `
Generate autocompletion scripts for hgctl for the specified shell.
`
const bashCompDesc = `
Generate the autocompletion script for the bash shell.
This script depends on the 'bash-completion' package.
If it is not installed already, you can install it via your OS's package manager.
To load completions in your current shell session:
source <(hgctl completion bash)
To load completions for every new session, execute once:
#### Linux:
hgctl completion bash > /etc/bash_completion.d/hgctl
#### macOS:
hgctl completion bash > $(brew --prefix)/etc/bash_completion.d/hgctl
You will need to start a new shell for this setup to take effect.
`
const zshCompDesc = `
Generate the autocompletion script for the zsh shell.
If shell completion is not already enabled in your environment you will need
to enable it. You can execute the following once:
echo "autoload -U compinit; compinit" >> ~/.zshrc
To load completions in your current shell session:
source <(hgctl completion zsh); compdef _hgctl hgctl
To load completions for every new session, execute once:
#### Linux:
hgctl completion zsh > "${fpath[1]}/_hgctl"
#### macOS:
hgctl completion zsh > $(brew --prefix)/share/zsh/site-functions/_hgctl
You will need to start a new shell for this setup to take effect.
`
const fishCompDesc = `
Generate the autocompletion script for the fish shell.
To load completions in your current shell session:
hgctl completion fish | source
To load completions for every new session, execute once:
hgctl completion fish > ~/.config/fish/completions/hgctl.fish
You will need to start a new shell for this setup to take effect.
`
const powershellCompDesc = `
Generate the autocompletion script for powershell.
To load completions in your current shell session:
hgctl completion powershell | Out-String | Invoke-Expression
To load completions for every new session, add the output of the above command
to your powershell profile.
`
const (
noDescFlagName = "no-descriptions"
noDescFlagText = "disable completion descriptions"
)
var disableCompDescriptions bool
// newCompletionCmd creates a new completion command for hgctl
func newCompletionCmd(out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "completion",
Short: "generate autocompletion scripts for the specified shell",
Long: completionDesc,
Args: cobra.NoArgs,
}
bash := &cobra.Command{
Use: "bash",
Short: "generate autocompletion script for bash",
Long: bashCompDesc,
Args: cobra.NoArgs,
ValidArgsFunction: noCompletions,
RunE: func(cmd *cobra.Command, args []string) error {
return runCompletionBash(out, cmd)
},
}
bash.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText)
zsh := &cobra.Command{
Use: "zsh",
Short: "generate autocompletion script for zsh",
Long: zshCompDesc,
Args: cobra.NoArgs,
ValidArgsFunction: noCompletions,
RunE: func(cmd *cobra.Command, args []string) error {
return runCompletionZsh(out, cmd)
},
}
zsh.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText)
fish := &cobra.Command{
Use: "fish",
Short: "generate autocompletion script for fish",
Long: fishCompDesc,
Args: cobra.NoArgs,
ValidArgsFunction: noCompletions,
RunE: func(cmd *cobra.Command, args []string) error {
return runCompletionFish(out, cmd)
},
}
fish.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText)
powershell := &cobra.Command{
Use: "powershell",
Short: "generate autocompletion script for powershell",
Long: powershellCompDesc,
Args: cobra.NoArgs,
ValidArgsFunction: noCompletions,
RunE: func(cmd *cobra.Command, args []string) error {
return runCompletionPowershell(out, cmd)
},
}
powershell.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText)
cmd.AddCommand(bash, zsh, fish, powershell)
return cmd
}
func runCompletionBash(out io.Writer, cmd *cobra.Command) error {
err := cmd.Root().GenBashCompletionV2(out, !disableCompDescriptions)
// In case the user renamed the hgctl binary, we hook the new binary name to the completion function
if binary := filepath.Base(os.Args[0]); binary != "hgctl" {
renamedBinaryHook := `
# Hook the command used to generate the completion script
# to the hgctl completion function to handle the case where
# the user renamed the hgctl binary
if [[ $(type -t compopt) = "builtin" ]]; then
complete -o default -F __start_hgctl %[1]s
else
complete -o default -o nospace -F __start_hgctl %[1]s
fi
`
fmt.Fprintf(out, renamedBinaryHook, binary)
}
return err
}
func runCompletionZsh(out io.Writer, cmd *cobra.Command) error {
var err error
if disableCompDescriptions {
err = cmd.Root().GenZshCompletionNoDesc(out)
} else {
err = cmd.Root().GenZshCompletion(out)
}
// In case the user renamed the hgctl binary, we hook the new binary name to the completion function
if binary := filepath.Base(os.Args[0]); binary != "hgctl" {
renamedBinaryHook := `
# Hook the command used to generate the completion script
# to the hgctl completion function to handle the case where
# the user renamed the hgctl binary
compdef _hgctl %[1]s
`
fmt.Fprintf(out, renamedBinaryHook, binary)
}
// Cobra doesn't source zsh completion file, explicitly doing it here
fmt.Fprintf(out, "compdef _hgctl hgctl")
return err
}
func runCompletionFish(out io.Writer, cmd *cobra.Command) error {
return cmd.Root().GenFishCompletion(out, !disableCompDescriptions)
}
func runCompletionPowershell(out io.Writer, cmd *cobra.Command) error {
if disableCompDescriptions {
return cmd.Root().GenPowerShellCompletion(out)
}
return cmd.Root().GenPowerShellCompletionWithDesc(out)
}
// Function to disable file completion
func noCompletions(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveNoFileComp
}

View File

@@ -96,7 +96,7 @@ func portForwarder(nn types.NamespacedName) (kubernetes.PortForwarder, error) {
return nil, fmt.Errorf("pod %s is not running", nn)
}
fw, err := kubernetes.NewLocalPortForwarder(c, nn, 0, defaultProxyAdminPort)
fw, err := kubernetes.NewLocalPortForwarder(c, nn, 0, defaultProxyAdminPort, bindAddress)
if err != nil {
return nil, err
}

View File

@@ -38,7 +38,7 @@ type fakePortForwarder struct {
}
func newFakePortForwarder(b []byte) (kubernetes.PortForwarder, error) {
p, err := kubernetes.LocalAvailablePort()
p, err := kubernetes.LocalAvailablePort("localhost")
if err != nil {
return nil, err
}

View File

@@ -15,15 +15,20 @@
package hgctl
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
"runtime"
"strings"
"github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
"github.com/alibaba/higress/pkg/cmd/options"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/flags"
types2 "github.com/docker/docker/api/types"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/types"
@@ -49,6 +54,8 @@ var (
envoyDashNs = ""
proxyAdminPort int
docker = false
)
const (
@@ -81,6 +88,7 @@ func newDashboardCmd() *cobra.Command {
"Default is true which means hgctl dashboard will always open a browser to view the dashboard.")
dashboardCmd.PersistentFlags().StringVarP(&addonNamespace, "namespace", "n", "higress-system",
"Namespace where the addon is running, if not specified, higress-system would be used")
dashboardCmd.PersistentFlags().StringVarP(&bindAddress, "listen", "l", "localhost", "The address to bind to")
prom := promDashCmd()
prom.PersistentFlags().IntVar(&promPort, "ui-port", defaultPrometheusPort, "The component dashboard UI port.")
@@ -91,7 +99,7 @@ func newDashboardCmd() *cobra.Command {
dashboardCmd.AddCommand(graf)
envoy := envoyDashCmd()
envoy.PersistentFlags().StringVarP(&labelSelector, "selector", "l", "app=higress-gateway", "Label selector")
envoy.PersistentFlags().StringVarP(&labelSelector, "selector", "s", "app=higress-gateway", "Label selector")
envoy.PersistentFlags().StringVarP(&envoyDashNs, "namespace", "n", "",
"Namespace where the addon is running, if not specified, higress-system would be used")
envoy.PersistentFlags().IntVar(&proxyAdminPort, "ui-port", defaultProxyAdminPort, "The component dashboard UI port.")
@@ -99,6 +107,7 @@ func newDashboardCmd() *cobra.Command {
consoleCmd := consoleDashCmd()
consoleCmd.PersistentFlags().IntVar(&consolePort, "ui-port", defaultConsolePort, "The component dashboard UI port.")
consoleCmd.PersistentFlags().BoolVar(&docker, "docker", false, "Search higress console from docker")
dashboardCmd.AddCommand(consoleCmd)
controllerDebugCmd := controllerDebugCmd()
@@ -156,18 +165,23 @@ func consoleDashCmd() *cobra.Command {
hgctl dash console
hgctl d console`,
RunE: func(cmd *cobra.Command, args []string) error {
if docker {
return accessDocker(cmd)
}
client, err := kubernetes.NewCLIClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
if err != nil {
return fmt.Errorf("build CLI client fail: %w", err)
fmt.Printf("build kubernetes CLI client fail: %v\ntry to access docker container\n", err)
return accessDocker(cmd)
}
pl, err := client.PodsForSelector(addonNamespace, "app.kubernetes.io/name=higress-console")
if err != nil {
return fmt.Errorf("not able to locate console pod: %v", err)
fmt.Printf("build kubernetes CLI client fail: %v\ntry to access docker container\n", err)
return accessDocker(cmd)
}
if len(pl.Items) < 1 {
return errors.New("no higress console pods found")
fmt.Printf("no higress console pods found\ntry to access docker container\n")
return accessDocker(cmd)
}
// only use the first pod in the list
@@ -179,6 +193,32 @@ func consoleDashCmd() *cobra.Command {
return cmd
}
// accessDocker access docker container
func accessDocker(cmd *cobra.Command) error {
dockerCli, err := command.NewDockerCli(command.WithCombinedStreams(os.Stdout))
if err != nil {
return fmt.Errorf("build docker CLI client fail: %w", err)
}
err = dockerCli.Initialize(flags.NewClientOptions())
if err != nil {
return fmt.Errorf("docker client initialize fail: %w", err)
}
apiClient := dockerCli.Client()
list, err := apiClient.ContainerList(context.Background(), types2.ContainerListOptions{})
for _, container := range list {
for i, name := range container.Names {
if strings.Contains(name, "higress-console") {
port := container.Ports[i].PublicPort
// not support define ip address
url := fmt.Sprintf("http://localhost:%d", port)
openBrowser(url, cmd.OutOrStdout(), browser)
return nil
}
}
}
return errors.New("no higress console container found")
}
// port-forward to Higress System Grafana; open browser
func grafanaDashCmd() *cobra.Command {
cmd := &cobra.Command{
@@ -324,7 +364,7 @@ func portForward(podName, namespace, flavor, urlFormat, localAddress string, rem
var err error
for _, localPort := range portPrefs {
var fw kubernetes.PortForwarder
fw, err = kubernetes.NewLocalPortForwarder(client, types.NamespacedName{Namespace: namespace, Name: podName}, localPort, remotePort)
fw, err = kubernetes.NewLocalPortForwarder(client, types.NamespacedName{Namespace: namespace, Name: podName}, localPort, remotePort, bindAddress)
if err != nil {
return fmt.Errorf("could not build port forwarder for %s: %v", flavor, err)
}
@@ -361,8 +401,6 @@ func ClosePortForwarderOnInterrupt(fw kubernetes.PortForwarder) {
}
func openBrowser(url string, writer io.Writer, browser bool) {
var err error
fmt.Fprintf(writer, "%s\n", url)
if !browser {
@@ -372,16 +410,30 @@ func openBrowser(url string, writer io.Writer, browser bool) {
switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", url).Start()
openCommand(writer, "xdg-open", url)
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
openCommand(writer, "rundll32", "url.dll,FileProtocolHandler", url)
case "darwin":
err = exec.Command("open", url).Start()
openCommand(writer, "open", url)
default:
fmt.Fprintf(writer, "Unsupported platform %q; open %s in your browser.\n", runtime.GOOS, url)
}
}
func openCommand(writer io.Writer, command string, args ...string) {
_, err := exec.LookPath(command)
if err != nil {
fmt.Fprintf(writer, "Failed to open browser; open %s in your browser.\nError: %s\n", url, err.Error())
if errors.Is(err, exec.ErrNotFound) {
fmt.Fprintf(writer, "Could not open your browser. Please open it maually.\n")
return
}
fmt.Fprintf(writer, "Failed to open browser; open %s in your browser.\nError: %s\n", args[0], err.Error())
return
}
err = exec.Command(command, args...).Start()
if err != nil {
fmt.Fprintf(writer, "Failed to open browser; open %s in your browser.\nError: %s\n", args[0], err.Error())
}
}

View File

@@ -318,7 +318,45 @@ func GenProfile(profileOrPath, fileOverlayYAML string, setFlags []string) (strin
return "", nil, err
}
finalProfile.InstallPackagePath = installPackagePath
if len(installPackagePath) > 0 {
finalProfile.InstallPackagePath = installPackagePath
}
if finalProfile.Profile == "" {
finalProfile.Profile = DefaultProfileName
}
return util.ToYAML(finalProfile), finalProfile, nil
}
func GenProfileFromProfileContent(profileContent, fileOverlayYAML string, setFlags []string) (string, *Profile, error) {
installPackagePath, err := getInstallPackagePath(fileOverlayYAML)
if err != nil {
return "", nil, err
}
if sfp := GetValueForSetFlag(setFlags, "installPackagePath"); sfp != "" {
// set flag installPackagePath has the highest precedence, if set.
installPackagePath = sfp
}
// Combine file and --set overlays and translate any K8s settings in values to Profile format
overlayYAML, err := overlaySetFlagValues(fileOverlayYAML, setFlags)
if err != nil {
return "", nil, err
}
// Merge user file and --set flags.
outYAML, err := util.OverlayYAML(profileContent, overlayYAML)
if err != nil {
return "", nil, fmt.Errorf("could not overlay user config over base: %s", err)
}
finalProfile, err := UnmarshalProfile(outYAML)
if err != nil {
return "", nil, err
}
if len(installPackagePath) > 0 {
finalProfile.InstallPackagePath = installPackagePath
}
if finalProfile.Profile == "" {
finalProfile.Profile = DefaultProfileName

View File

@@ -35,6 +35,7 @@ const (
type Profile struct {
Profile string `json:"profile,omitempty"`
InstallPackagePath string `json:"installPackagePath,omitempty"`
HigressVersion string `json:"higressVersion,omitempty"`
Global ProfileGlobal `json:"global,omitempty"`
Console ProfileConsole `json:"console,omitempty"`
Gateway ProfileGateway `json:"gateway,omitempty"`

View File

@@ -38,6 +38,7 @@ import (
"helm.sh/helm/v3/pkg/engine"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/repo"
"k8s.io/client-go/rest"
"sigs.k8s.io/yaml"
)
@@ -134,6 +135,12 @@ type RendererOptions struct {
// fields for RemoteRenderer
Version string
RepoURL string
// Capabilities
Capabilities *chartutil.Capabilities
// rest config
restConfig *rest.Config
}
type RendererOption func(*RendererOptions)
@@ -174,6 +181,18 @@ func WithRepoURL(repo string) RendererOption {
}
}
func WithCapabilities(capabilities *chartutil.Capabilities) RendererOption {
return func(opts *RendererOptions) {
opts.Capabilities = capabilities
}
}
func WithRestConfig(config *rest.Config) RendererOption {
return func(opts *RendererOptions) {
opts.restConfig = config
}
}
// LocalFileRenderer load yaml files from local file system
type LocalFileRenderer struct {
Opts *RendererOptions
@@ -418,8 +437,11 @@ func renderManifest(valsYaml string, cht *chart.Chart, builtIn bool, opts *Rende
Name: opts.Name,
Namespace: opts.Namespace,
}
// TODO need to specify k8s version
caps := chartutil.DefaultCapabilities
var caps *chartutil.Capabilities
caps = opts.Capabilities
if caps == nil {
caps = chartutil.DefaultCapabilities
}
// maybe we need a configuration to change this caps
resVals, err := chartutil.ToRenderValues(cht, valsMap, RelOpts, caps)
if err != nil {
@@ -428,7 +450,7 @@ func renderManifest(valsYaml string, cht *chart.Chart, builtIn bool, opts *Rende
if builtIn {
resVals["Values"].(chartutil.Values)["enabled"] = true
}
filesMap, err := engine.Render(cht, resVals)
filesMap, err := engine.RenderWithClient(cht, resVals, opts.restConfig)
if err != nil {
return "", fmt.Errorf("Render chart failed err: %s", err)
}

View File

@@ -17,6 +17,7 @@ package hgctl
import (
"fmt"
"io"
"os"
"strings"
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
@@ -134,7 +135,7 @@ func install(writer io.Writer, iArgs *InstallArgs) error {
return fmt.Errorf("generate config: %v", err)
}
fmt.Fprintf(writer, "🧐 Validating Profile: \"%s\" \n", profileName)
fmt.Fprintf(writer, "\n🧐 Validating Profile: \"%s\" \n", profileName)
err = profile.Validate()
if err != nil {
return err
@@ -144,6 +145,12 @@ func install(writer io.Writer, iArgs *InstallArgs) error {
if err != nil {
return fmt.Errorf("failed to install manifests: %v", err)
}
// Remove "~/.hgctl/profiles/install.yaml"
if oldProfileName, isExisted := installer.GetInstalledYamlPath(); isExisted {
_ = os.Remove(oldProfileName)
}
return nil
}

View File

@@ -17,6 +17,7 @@ package installer
import (
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
"github.com/alibaba/higress/pkg/cmd/hgctl/util"
"helm.sh/helm/v3/pkg/chartutil"
"sigs.k8s.io/yaml"
)
@@ -49,6 +50,8 @@ type ComponentOptions struct {
ChartName string
Version string
Quiet bool
// Capabilities
Capabilities *chartutil.Capabilities
}
type ComponentOption func(*ComponentOptions)
@@ -83,6 +86,12 @@ func WithComponentVersion(version string) ComponentOption {
}
}
func WithComponentCapabilities(capabilities *chartutil.Capabilities) ComponentOption {
return func(opts *ComponentOptions) {
opts.Capabilities = capabilities
}
}
func WithQuiet() ComponentOption {
return func(opts *ComponentOptions) {
opts.Quiet = true

View File

@@ -21,6 +21,7 @@ import (
"strings"
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
"github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
"github.com/alibaba/higress/pkg/cmd/hgctl/manifests"
)
@@ -34,9 +35,10 @@ type GatewayAPIComponent struct {
opts *ComponentOptions
renderer helm.Renderer
writer io.Writer
kubeCli kubernetes.CLIClient
}
func NewGatewayAPIComponent(profile *helm.Profile, writer io.Writer, opts ...ComponentOption) (Component, error) {
func NewGatewayAPIComponent(kubeCli kubernetes.CLIClient, profile *helm.Profile, writer io.Writer, opts ...ComponentOption) (Component, error) {
newOpts := &ComponentOptions{}
for _, opt := range opts {
opt(newOpts)
@@ -55,6 +57,8 @@ func NewGatewayAPIComponent(profile *helm.Profile, writer io.Writer, opts ...Com
helm.WithVersion(newOpts.Version),
helm.WithFS(manifests.BuiltinOrDir("")),
helm.WithDir(chartDir),
helm.WithCapabilities(newOpts.Capabilities),
helm.WithRestConfig(kubeCli.RESTConfig()),
)
if err != nil {
return nil, err
@@ -65,6 +69,7 @@ func NewGatewayAPIComponent(profile *helm.Profile, writer io.Writer, opts ...Com
renderer: renderer,
opts: newOpts,
writer: writer,
kubeCli: kubeCli,
}
return gatewayAPIComponent, nil
}

View File

@@ -17,8 +17,10 @@ package installer
import (
"errors"
"fmt"
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
"io"
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
"github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
)
const (
@@ -31,6 +33,7 @@ type HigressComponent struct {
opts *ComponentOptions
renderer helm.Renderer
writer io.Writer
kubeCli kubernetes.CLIClient
}
func (h *HigressComponent) ComponentName() ComponentName {
@@ -67,6 +70,7 @@ func (h *HigressComponent) Run() error {
if err := h.renderer.Init(); err != nil {
return err
}
h.profile.HigressVersion = h.opts.Version
h.started = true
return nil
}
@@ -89,7 +93,7 @@ func (h *HigressComponent) RenderManifest() (string, error) {
return manifest, nil
}
func NewHigressComponent(profile *helm.Profile, writer io.Writer, opts ...ComponentOption) (Component, error) {
func NewHigressComponent(kubeCli kubernetes.CLIClient, profile *helm.Profile, writer io.Writer, opts ...ComponentOption) (Component, error) {
newOpts := &ComponentOptions{}
for _, opt := range opts {
opt(newOpts)
@@ -105,6 +109,8 @@ func NewHigressComponent(profile *helm.Profile, writer io.Writer, opts ...Compon
helm.WithNamespace(newOpts.Namespace),
helm.WithRepoURL(newOpts.RepoURL),
helm.WithVersion(newOpts.Version),
helm.WithCapabilities(newOpts.Capabilities),
helm.WithRestConfig(kubeCli.RESTConfig()),
)
if err != nil {
return nil, err
@@ -115,6 +121,7 @@ func NewHigressComponent(profile *helm.Profile, writer io.Writer, opts ...Compon
renderer: renderer,
opts: newOpts,
writer: writer,
kubeCli: kubeCli,
}
return higressComponent, nil
}

View File

@@ -17,17 +17,17 @@ package installer
import (
"errors"
"fmt"
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
"github.com/alibaba/higress/pkg/cmd/hgctl/util"
"io"
"os"
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
)
type DockerInstaller struct {
started bool
standalone *StandaloneComponent
profile *helm.Profile
writer io.Writer
started bool
standalone *StandaloneComponent
profile *helm.Profile
writer io.Writer
profileStore ProfileStore
}
func (d *DockerInstaller) Install() error {
@@ -37,11 +37,11 @@ func (d *DockerInstaller) Install() error {
return err
}
profileName, _ := GetInstalledYamlPath()
fmt.Fprintf(d.writer, "\n✔ Wrote Profile: \"%s\" \n", profileName)
if err := util.WriteFileString(profileName, util.ToYAML(d.profile), 0o644); err != nil {
return err
profileName, err1 := d.profileStore.Save(d.profile)
if err1 != nil {
return err1
}
fmt.Fprintf(d.writer, "\n✔ Wrote Profile: \"%s\" \n", profileName)
fmt.Fprintf(d.writer, "\n🎊 Install All Resources Complete!\n")
return nil
@@ -55,9 +55,11 @@ func (d *DockerInstaller) UnInstall() error {
return err
}
profileName, _ := GetInstalledYamlPath()
profileName, err1 := d.profileStore.Delete(d.profile)
if err1 != nil {
return err1
}
fmt.Fprintf(d.writer, "\n✔ Removed Profile: \"%s\" \n", profileName)
os.Remove(profileName)
fmt.Fprintf(d.writer, "\n🎊 Uninstall All Resources Complete!\n")
return nil
@@ -92,10 +94,19 @@ func NewDockerInstaller(profile *helm.Profile, writer io.Writer, quiet bool) (*D
return nil, fmt.Errorf("NewStandaloneComponent failed, err: %s", err)
}
profileInstalledPath, err1 := GetProfileInstalledPath()
if err1 != nil {
return nil, err1
}
profileStore, err2 := NewFileDirProfileStore(profileInstalledPath)
if err2 != nil {
return nil, err2
}
op := &DockerInstaller{
profile: profile,
standalone: standaloneComponent,
writer: writer,
profile: profile,
standalone: standaloneComponent,
writer: writer,
profileStore: profileStore,
}
return op, nil
}

View File

@@ -19,6 +19,8 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
"github.com/alibaba/higress/pkg/cmd/hgctl/helm/object"
@@ -27,11 +29,12 @@ import (
)
type K8sInstaller struct {
started bool
components map[ComponentName]Component
kubeCli kubernetes.CLIClient
profile *helm.Profile
writer io.Writer
started bool
components map[ComponentName]Component
kubeCli kubernetes.CLIClient
profile *helm.Profile
writer io.Writer
profileStore ProfileStore
}
func (o *K8sInstaller) Install() error {
@@ -43,10 +46,6 @@ func (o *K8sInstaller) Install() error {
return nil
}
if _, err := GetProfileInstalledPath(); err != nil {
return err
}
if err := o.Run(); err != nil {
return err
}
@@ -61,11 +60,16 @@ func (o *K8sInstaller) Install() error {
return err
}
profileName, _ := GetInstalledYamlPath()
fmt.Fprintf(o.writer, "\n✔ Wrote Profile: \"%s\" \n", profileName)
if err := util.WriteFileString(profileName, util.ToYAML(o.profile), 0o644); err != nil {
return err
profileName, err1 := o.profileStore.Save(o.profile)
if err1 != nil {
return err1
}
fmt.Fprintf(o.writer, "\n✔ Wrote Profile in kubernetes configmap: \"%s\" \n", profileName)
fmt.Fprintf(o.writer, "\n Use bellow kubectl command to edit profile for upgrade. \n")
fmt.Fprintf(o.writer, " ================================================================================== \n")
names := strings.Split(profileName, "/")
fmt.Fprintf(o.writer, " kubectl edit configmap %s -n %s \n", names[1], names[0])
fmt.Fprintf(o.writer, " ================================================================================== \n")
fmt.Fprintf(o.writer, "\n🎊 Install All Resources Complete!\n")
@@ -91,9 +95,11 @@ func (o *K8sInstaller) UnInstall() error {
return err
}
profileName, _ := GetInstalledYamlPath()
profileName, err1 := o.profileStore.Delete(o.profile)
if err1 != nil {
return err1
}
fmt.Fprintf(o.writer, "\n✔ Removed Profile: \"%s\" \n", profileName)
os.Remove(profileName)
fmt.Fprintf(o.writer, "\n🎊 Uninstall All Resources Complete!\n")
@@ -202,6 +208,19 @@ func (o *K8sInstaller) DeleteManifests(manifestMap map[ComponentName]string) err
return nil
}
// WriteManifests write component manifests to local files
func (o *K8sInstaller) WriteManifests(manifestMap map[ComponentName]string) error {
if o.kubeCli == nil {
return errors.New("no injected k8s cli into K8sInstaller")
}
rootPath, _ := os.Getwd()
for name, manifest := range manifestMap {
fileName := filepath.Join(rootPath, string(name)+".yaml")
util.WriteFileString(fileName, manifest, 0o644)
}
return nil
}
// deleteManifest delete manifest to certain namespace
func (o *K8sInstaller) deleteManifest(manifest string, ns string) error {
objs, err := object.ParseK8sObjectsFromYAMLManifest(manifest)
@@ -239,6 +258,14 @@ func NewK8sInstaller(profile *helm.Profile, cli kubernetes.CLIClient, writer io.
if profile == nil {
return nil, errors.New("install profile is empty")
}
// initialize server info
serverInfo, _ := NewServerInfo(cli)
fmt.Fprintf(writer, "\n⌛ Detecting kubernetes version ... ")
capabilities, err := serverInfo.GetCapabilities()
if err != nil {
return nil, err
}
fmt.Fprintf(writer, "%s\n", capabilities.KubeVersion.Version)
// initialize components
components := make(map[ComponentName]Component)
opts := []ComponentOption{
@@ -247,11 +274,12 @@ func NewK8sInstaller(profile *helm.Profile, cli kubernetes.CLIClient, writer io.
WithComponentVersion(profile.Charts.Higress.Version),
WithComponentRepoURL(profile.Charts.Higress.Url),
WithComponentChartName(profile.Charts.Higress.Name),
WithComponentCapabilities(capabilities),
}
if quiet {
opts = append(opts, WithQuiet())
}
higressComponent, err := NewHigressComponent(profile, writer, opts...)
higressComponent, err := NewHigressComponent(cli, profile, writer, opts...)
if err != nil {
return nil, fmt.Errorf("NewHigressComponent failed, err: %s", err)
}
@@ -267,12 +295,13 @@ func NewK8sInstaller(profile *helm.Profile, cli kubernetes.CLIClient, writer io.
WithComponentVersion("1.18.2"),
WithComponentRepoURL("embed://istiobase"),
WithComponentChartName("istio"),
WithComponentCapabilities(capabilities),
}
if quiet {
opts = append(opts, WithQuiet())
}
istioCRDComponent, err := NewIstioCRDComponent(profile, writer, opts...)
istioCRDComponent, err := NewIstioCRDComponent(cli, profile, writer, opts...)
if err != nil {
return nil, fmt.Errorf("NewIstioCRDComponent failed, err: %s", err)
}
@@ -285,23 +314,30 @@ func NewK8sInstaller(profile *helm.Profile, cli kubernetes.CLIClient, writer io.
WithComponentVersion("1.0.0"),
WithComponentRepoURL("embed://gatewayapi"),
WithComponentChartName("gatewayAPI"),
WithComponentCapabilities(capabilities),
}
if quiet {
opts = append(opts, WithQuiet())
}
gatewayAPIComponent, err := NewGatewayAPIComponent(profile, writer, opts...)
gatewayAPIComponent, err := NewGatewayAPIComponent(cli, profile, writer, opts...)
if err != nil {
return nil, fmt.Errorf("NewGatewayAPIComponent failed, err: %s", err)
}
components[GatewayAPI] = gatewayAPIComponent
}
profileStore, err := NewConfigmapProfileStore(cli)
if err != nil {
return nil, err
}
op := &K8sInstaller{
profile: profile,
components: components,
kubeCli: cli,
writer: writer,
profile: profile,
components: components,
kubeCli: cli,
writer: writer,
profileStore: profileStore,
}
return op, nil
}

View File

@@ -20,6 +20,7 @@ import (
"strings"
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
"github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
"github.com/alibaba/higress/pkg/cmd/hgctl/manifests"
)
@@ -33,9 +34,10 @@ type IstioCRDComponent struct {
opts *ComponentOptions
renderer helm.Renderer
writer io.Writer
kubeCli kubernetes.CLIClient
}
func NewIstioCRDComponent(profile *helm.Profile, writer io.Writer, opts ...ComponentOption) (Component, error) {
func NewIstioCRDComponent(kubeCli kubernetes.CLIClient, profile *helm.Profile, writer io.Writer, opts ...ComponentOption) (Component, error) {
newOpts := &ComponentOptions{}
for _, opt := range opts {
opt(newOpts)
@@ -54,6 +56,8 @@ func NewIstioCRDComponent(profile *helm.Profile, writer io.Writer, opts ...Compo
helm.WithVersion(newOpts.Version),
helm.WithFS(manifests.BuiltinOrDir("")),
helm.WithDir(chartDir),
helm.WithCapabilities(newOpts.Capabilities),
helm.WithRestConfig(kubeCli.RESTConfig()),
)
if err != nil {
return nil, err
@@ -64,6 +68,8 @@ func NewIstioCRDComponent(profile *helm.Profile, writer io.Writer, opts ...Compo
helm.WithNamespace(newOpts.Namespace),
helm.WithRepoURL(newOpts.RepoURL),
helm.WithVersion(newOpts.Version),
helm.WithCapabilities(newOpts.Capabilities),
helm.WithRestConfig(kubeCli.RESTConfig()),
)
if err != nil {
return nil, err
@@ -75,6 +81,7 @@ func NewIstioCRDComponent(profile *helm.Profile, writer io.Writer, opts ...Compo
renderer: renderer,
opts: newOpts,
writer: writer,
kubeCli: kubeCli,
}
return istioComponent, nil
}

View File

@@ -0,0 +1,247 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// 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 installer
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
"github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
"github.com/alibaba/higress/pkg/cmd/hgctl/util"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
ProfileConfigmapKey = "profile"
ProfileConfigmapName = "higress-profile"
ProfileConfigmapAnnotation = "higress.io/install"
ProfileFilePrefix = "install"
)
type ProfileContext struct {
Profile *helm.Profile
SourceType string
Namespace string
PathOrName string
Install helm.InstallMode
HigressVersion string
}
type ProfileStore interface {
Save(profile *helm.Profile) (string, error)
List() ([]*ProfileContext, error)
Delete(profile *helm.Profile) (string, error)
}
type FileDirProfileStore struct {
profilesPath string
}
func (f *FileDirProfileStore) Save(profile *helm.Profile) (string, error) {
namespace := profile.Global.Namespace
install := profile.Global.Install
var profileName = ""
if install == helm.InstallK8s || install == helm.InstallLocalK8s {
profileName = filepath.Join(f.profilesPath, fmt.Sprintf("%s-%s.yaml", ProfileFilePrefix, namespace))
} else {
profileName = filepath.Join(f.profilesPath, fmt.Sprintf("%s-%s.yaml", ProfileFilePrefix, install))
}
if err := util.WriteFileString(profileName, util.ToYAML(profile), 0o644); err != nil {
return "", err
}
return profileName, nil
}
func (f *FileDirProfileStore) List() ([]*ProfileContext, error) {
profileContexts := make([]*ProfileContext, 0)
dir, err := os.ReadDir(f.profilesPath)
if err != nil {
return nil, err
}
for _, file := range dir {
if !strings.HasSuffix(file.Name(), ".yaml") {
continue
}
if file.IsDir() {
continue
}
fileName := filepath.Join(f.profilesPath, file.Name())
content, err2 := os.ReadFile(fileName)
if err2 != nil {
continue
}
profile, err3 := helm.UnmarshalProfile(string(content))
if err3 != nil {
continue
}
profileContext := &ProfileContext{
Profile: profile,
Namespace: profile.Global.Namespace,
Install: profile.Global.Install,
HigressVersion: profile.HigressVersion,
SourceType: "file",
PathOrName: fileName,
}
profileContexts = append(profileContexts, profileContext)
}
return profileContexts, nil
}
func (f *FileDirProfileStore) Delete(profile *helm.Profile) (string, error) {
namespace := profile.Global.Namespace
install := profile.Global.Install
var profileName = ""
if install == helm.InstallK8s || install == helm.InstallLocalK8s {
profileName = filepath.Join(f.profilesPath, fmt.Sprintf("%s-%s.yaml", ProfileFilePrefix, namespace))
} else {
profileName = filepath.Join(f.profilesPath, fmt.Sprintf("%s-%s.yaml", ProfileFilePrefix, install))
}
if err := os.Remove(profileName); err != nil {
return "", err
}
return profileName, nil
}
func NewFileDirProfileStore(profilesPath string) (ProfileStore, error) {
if _, err := os.Stat(profilesPath); os.IsNotExist(err) {
if err = os.MkdirAll(profilesPath, os.ModePerm); err != nil {
return nil, err
}
}
profileStore := &FileDirProfileStore{
profilesPath: profilesPath,
}
return profileStore, nil
}
type ConfigmapProfileStore struct {
kubeCli kubernetes.CLIClient
}
func (c *ConfigmapProfileStore) Save(profile *helm.Profile) (string, error) {
bytes, err := json.Marshal(profile)
jsonProfile := ""
if err == nil {
jsonProfile = string(bytes)
}
annotation := make(map[string]string, 0)
annotation[ProfileConfigmapAnnotation] = jsonProfile
configmap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: profile.Global.Namespace,
Name: ProfileConfigmapName,
Annotations: annotation,
},
}
configmap.Data = make(map[string]string, 0)
configmap.Data[ProfileConfigmapKey] = util.ToYAML(profile)
name := fmt.Sprintf("%s/%s", profile.Global.Namespace, ProfileConfigmapName)
if err := c.applyConfigmap(configmap); err != nil {
return "", err
}
return name, nil
}
func (c *ConfigmapProfileStore) List() ([]*ProfileContext, error) {
profileContexts := make([]*ProfileContext, 0)
configmapList, err := c.listConfigmaps(ProfileConfigmapName, "", 100)
if err != nil {
return profileContexts, err
}
for _, configmap := range configmapList.Items {
if data, ok := configmap.Data[ProfileConfigmapKey]; ok {
profile, err := helm.UnmarshalProfile(data)
if err != nil {
continue
}
profileContext := &ProfileContext{
Profile: profile,
Namespace: profile.Global.Namespace,
Install: profile.Global.Install,
HigressVersion: profile.HigressVersion,
SourceType: "configmap",
PathOrName: fmt.Sprintf("%s/%s", profile.Global.Namespace, configmap.Name),
}
profileContexts = append(profileContexts, profileContext)
}
}
return profileContexts, nil
}
func (c *ConfigmapProfileStore) Delete(profile *helm.Profile) (string, error) {
configmap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: profile.Global.Namespace,
Name: ProfileConfigmapName,
},
}
name := fmt.Sprintf("%s/%s", profile.Global.Namespace, ProfileConfigmapName)
if err := c.deleteConfigmap(configmap); err != nil {
return "", err
}
return name, nil
}
func (c *ConfigmapProfileStore) listConfigmaps(name string, namespace string, size int64) (*corev1.ConfigMapList, error) {
var result *corev1.ConfigMapList
var err error
if len(namespace) == 0 {
result, err = c.kubeCli.KubernetesInterface().CoreV1().ConfigMaps("").List(context.Background(), metav1.ListOptions{Limit: size, FieldSelector: fmt.Sprintf("metadata.name=%s", name)})
} else {
result, err = c.kubeCli.KubernetesInterface().CoreV1().ConfigMaps(namespace).List(context.Background(), metav1.ListOptions{Limit: size, FieldSelector: fmt.Sprintf("metadata.name=%s", name)})
}
if err != nil {
return nil, err
}
return result, nil
}
func (c *ConfigmapProfileStore) applyConfigmap(configmap *corev1.ConfigMap) error {
_, err := c.kubeCli.KubernetesInterface().CoreV1().ConfigMaps(configmap.Namespace).Get(context.Background(), configmap.Name, metav1.GetOptions{})
if err != nil && errors.IsNotFound(err) {
_, err = c.kubeCli.KubernetesInterface().CoreV1().ConfigMaps(configmap.Namespace).Create(context.Background(), configmap, metav1.CreateOptions{})
return err
} else if err != nil {
return err
} else {
_, err = c.kubeCli.KubernetesInterface().CoreV1().ConfigMaps(configmap.Namespace).Update(context.Background(), configmap, metav1.UpdateOptions{})
return err
}
}
func (c *ConfigmapProfileStore) deleteConfigmap(configmap *corev1.ConfigMap) error {
err := c.kubeCli.KubernetesInterface().CoreV1().ConfigMaps(configmap.Namespace).Delete(context.Background(), configmap.Name, metav1.DeleteOptions{})
if err != nil {
if !errors.IsNotFound(err) {
return err
}
}
return nil
}
func NewConfigmapProfileStore(kubeCli kubernetes.CLIClient) (ProfileStore, error) {
profileStore := &ConfigmapProfileStore{
kubeCli: kubeCli,
}
return profileStore, nil
}

View File

@@ -0,0 +1,66 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// 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 installer
import (
"github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
"github.com/pkg/errors"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chartutil"
"k8s.io/client-go/discovery"
)
type ServerInfo struct {
kubeCli kubernetes.CLIClient
}
func (c *ServerInfo) GetCapabilities() (*chartutil.Capabilities, error) {
// force a discovery cache invalidation to always fetch the latest server version/capabilities.
dc := c.kubeCli.KubernetesInterface().Discovery()
kubeVersion, err := dc.ServerVersion()
if err != nil {
return nil, errors.Wrap(err, "could not get server version from Kubernetes")
}
// Issue #6361:
// Client-Go emits an error when an API service is registered but unimplemented.
// We trap that error here and print a warning. But since the discovery client continues
// building the API object, it is correctly populated with all valid APIs.
// See https://github.com/kubernetes/kubernetes/issues/72051#issuecomment-521157642
apiVersions, err := action.GetVersionSet(dc)
if err != nil {
if discovery.IsGroupDiscoveryFailedError(err) {
} else {
return nil, errors.Wrap(err, "could not get apiVersions from Kubernetes")
}
}
capabilities := &chartutil.Capabilities{
APIVersions: apiVersions,
KubeVersion: chartutil.KubeVersion{
Version: kubeVersion.GitVersion,
Major: kubeVersion.Major,
Minor: kubeVersion.Minor,
},
HelmVersion: chartutil.DefaultCapabilities.HelmVersion,
}
return capabilities, nil
}
func NewServerInfo(kubCli kubernetes.CLIClient) (*ServerInfo, error) {
serverInfo := &ServerInfo{
kubeCli: kubCli,
}
return serverInfo, nil
}

View File

@@ -58,7 +58,10 @@ func (s *StandaloneComponent) Install() error {
if err := s.agent.Install(); err != nil {
return err
}
// Set Higress version
if version, err := s.agent.Version(); err == nil {
s.profile.HigressVersion = version
}
return nil
}
@@ -86,7 +89,10 @@ func (s *StandaloneComponent) Upgrade() error {
if err := s.agent.Upgrade(); err != nil {
return err
}
// Set Higress version
if version, err := s.agent.Version(); err != nil {
s.profile.HigressVersion = version
}
return nil
}
@@ -112,10 +118,6 @@ func NewStandaloneComponent(profile *helm.Profile, writer io.Writer, opts ...Com
}
func prepareProfile(profile *helm.Profile) error {
if _, err := GetProfileInstalledPath(); err != nil {
return err
}
if len(profile.InstallPackagePath) == 0 {
dir, err := GetDefaultInstallPackagePath()
if err != nil {

View File

@@ -57,6 +57,9 @@ type CLIClient interface {
// CreateNamespace create namespace
CreateNamespace(namespace string) error
// KubernetesInterface get kubernetes interface
KubernetesInterface() kubernetes.Interface
}
var _ CLIClient = &client{}
@@ -246,3 +249,9 @@ func (c *client) CreateNamespace(namespace string) error {
return nil
}
// KubernetesInterface get kubernetes interface
func (c *client) KubernetesInterface() kubernetes.Interface {
return c.kube
}

View File

@@ -28,12 +28,8 @@ import (
"k8s.io/client-go/transport/spdy"
)
const (
DefaultLocalAddress = "localhost"
)
func LocalAvailablePort() (int, error) {
l, err := net.Listen("tcp", fmt.Sprintf("%s:0", DefaultLocalAddress))
func LocalAvailablePort(localAddress string) (int, error) {
l, err := net.Listen("tcp", fmt.Sprintf("%s:0", localAddress))
if err != nil {
return 0, err
}
@@ -59,23 +55,25 @@ type localForwarder struct {
types.NamespacedName
CLIClient
localPort int
podPort int
localPort int
podPort int
localAddress string
stopCh chan struct{}
}
func NewLocalPortForwarder(client CLIClient, namespacedName types.NamespacedName, localPort, podPort int) (PortForwarder, error) {
func NewLocalPortForwarder(client CLIClient, namespacedName types.NamespacedName, localPort, podPort int, bindAddress string) (PortForwarder, error) {
f := &localForwarder{
stopCh: make(chan struct{}),
CLIClient: client,
NamespacedName: namespacedName,
localPort: localPort,
podPort: podPort,
localAddress: bindAddress,
}
if f.localPort == 0 {
// get a random port
p, err := LocalAvailablePort()
p, err := LocalAvailablePort(bindAddress)
if err != nil {
return nil, errors.Wrapf(err, "failed to get a local available port")
}
@@ -136,7 +134,7 @@ func (f *localForwarder) buildKubernetesPortForwarder(readyCh chan struct{}) (*p
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: roundTripper}, http.MethodPost, serverURL)
fw, err := portforward.NewOnAddresses(dialer,
[]string{DefaultLocalAddress},
[]string{f.localAddress},
[]string{fmt.Sprintf("%d:%d", f.localPort, f.podPort)},
f.stopCh,
readyCh,
@@ -154,7 +152,7 @@ func (f *localForwarder) Stop() {
}
func (f *localForwarder) Address() string {
return fmt.Sprintf("%s:%d", DefaultLocalAddress, f.localPort)
return fmt.Sprintf("%s:%d", f.localAddress, f.localPort)
}
func (f *localForwarder) WaitForStop() {

View File

@@ -17,6 +17,7 @@ package hgctl
import (
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin"
"github.com/spf13/cobra"
"os"
)
// GetRootCommand returns the root cobra command to be executed
@@ -38,6 +39,7 @@ func GetRootCommand() *cobra.Command {
rootCmd.AddCommand(newDashboardCmd())
rootCmd.AddCommand(newManifestCmd())
rootCmd.AddCommand(plugin.NewCommand())
rootCmd.AddCommand(newCompletionCmd(os.Stdout))
return rootCmd
}

View File

@@ -17,22 +17,24 @@ package hgctl
import (
"fmt"
"io"
"os"
"strings"
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
"github.com/alibaba/higress/pkg/cmd/hgctl/installer"
"github.com/alibaba/higress/pkg/cmd/hgctl/util"
"github.com/alibaba/higress/pkg/cmd/options"
"github.com/spf13/cobra"
)
type uninstallArgs struct {
// purgeIstioCRD delete all of Istio resources.
purgeIstioCRD bool
// purgeResources delete all of installed resources.
purgeResources bool
}
func addUninstallFlags(cmd *cobra.Command, args *uninstallArgs) {
cmd.PersistentFlags().BoolVarP(&args.purgeIstioCRD, "purge-istio-crd", "", false,
"Delete all of Istio resources")
cmd.PersistentFlags().BoolVarP(&args.purgeResources, "purge-resources", "", false,
"Delete all of IstioAPI,GatewayAPI resources")
}
// newUninstallCmd command uninstalls Istio from a cluster
@@ -42,11 +44,11 @@ func newUninstallCmd() *cobra.Command {
Use: "uninstall",
Short: "Uninstall higress from a cluster",
Long: "The uninstall command uninstalls higress from a cluster or local environment",
Example: ` # Uninstall higress
Example: `# Uninstall higress
hgctl uninstal
# Uninstall higress and istio CRD from a cluster
hgctl uninstall --purge-istio-crd
# Uninstall higress, istioAPI and GatewayAPI from a cluster
hgctl uninstall --purge-resources
`,
RunE: func(cmd *cobra.Command, args []string) error {
return uninstall(cmd.OutOrStdout(), uiArgs)
@@ -60,18 +62,22 @@ func newUninstallCmd() *cobra.Command {
// uninstall uninstalls control plane by either pruning by target revision or deleting specified manifests.
func uninstall(writer io.Writer, uiArgs *uninstallArgs) error {
profileName, ok := installer.GetInstalledYamlPath()
if !ok {
fmt.Fprintf(writer, "⌛️ Checking higress installed profiles...\n")
profileContexts, _ := getAllProfiles()
if len(profileContexts) == 0 {
fmt.Fprintf(writer, "\nHigress hasn't been installed yet!\n")
return nil
}
setFlags := make([]string, 0)
_, profile, err := helm.GenProfile(profileName, "", setFlags)
profileContext := promptProfileContexts(writer, profileContexts)
_, profile, err := helm.GenProfileFromProfileContent(util.ToYAML(profileContext.Profile), "", setFlags)
if err != nil {
return err
}
fmt.Fprintf(writer, "🧐 Validating Profile: \"%s\" \n", profileName)
fmt.Fprintf(writer, "\n🧐 Validating Profile: \"%s\" \n", profileContext.PathOrName)
err = profile.Validate()
if err != nil {
return err
@@ -82,7 +88,12 @@ func uninstall(writer io.Writer, uiArgs *uninstallArgs) error {
}
if profile.Global.Install == helm.InstallK8s || profile.Global.Install == helm.InstallLocalK8s {
profile.Global.EnableIstioAPI = uiArgs.purgeIstioCRD
if profile.Global.EnableIstioAPI {
profile.Global.EnableIstioAPI = uiArgs.purgeResources
}
if profile.Global.EnableGatewayAPI {
profile.Global.EnableGatewayAPI = uiArgs.purgeResources
}
}
err = uninstallManifests(profile, writer, uiArgs)
@@ -90,6 +101,11 @@ func uninstall(writer io.Writer, uiArgs *uninstallArgs) error {
return err
}
// Remove "~/.hgctl/profiles/install.yaml"
if oldProfileName, isExisted := installer.GetInstalledYamlPath(); isExisted {
_ = os.Remove(oldProfileName)
}
return nil
}

View File

@@ -17,10 +17,14 @@ package hgctl
import (
"fmt"
"io"
"os"
"strconv"
"strings"
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
"github.com/alibaba/higress/pkg/cmd/hgctl/installer"
"github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
"github.com/alibaba/higress/pkg/cmd/hgctl/util"
"github.com/alibaba/higress/pkg/cmd/options"
"github.com/spf13/cobra"
)
@@ -58,8 +62,9 @@ func newUpgradeCmd() *cobra.Command {
// upgrade upgrade higress resources from the cluster.
func upgrade(writer io.Writer, iArgs *InstallArgs) error {
setFlags := applyFlagAliases(iArgs.Set, iArgs.ManifestsPath)
profileName, ok := installer.GetInstalledYamlPath()
if !ok {
fmt.Fprintf(writer, "⌛️ Checking higress installed profiles...\n")
profileContexts, _ := getAllProfiles()
if len(profileContexts) == 0 {
fmt.Fprintf(writer, "\nHigress hasn't been installed yet!\n")
return nil
}
@@ -69,12 +74,14 @@ func upgrade(writer io.Writer, iArgs *InstallArgs) error {
return err
}
_, profile, err := helm.GenProfile(profileName, valuesOverlay, setFlags)
profileContext := promptProfileContexts(writer, profileContexts)
_, profile, err := helm.GenProfileFromProfileContent(util.ToYAML(profileContext.Profile), valuesOverlay, setFlags)
if err != nil {
return err
}
fmt.Fprintf(writer, "🧐 Validating Profile: \"%s\" \n", profileName)
fmt.Fprintf(writer, "\n🧐 Validating Profile: \"%s\" \n", profileContext.PathOrName)
err = profile.Validate()
if err != nil {
return err
@@ -89,6 +96,11 @@ func upgrade(writer io.Writer, iArgs *InstallArgs) error {
return err
}
// Remove "~/.hgctl/profiles/install.yaml"
if oldProfileName, isExisted := installer.GetInstalledYamlPath(); isExisted {
_ = os.Remove(oldProfileName)
}
return nil
}
@@ -121,3 +133,71 @@ func upgradeManifests(profile *helm.Profile, writer io.Writer) error {
return nil
}
func getAllProfiles() ([]*installer.ProfileContext, error) {
profileContexts := make([]*installer.ProfileContext, 0)
profileInstalledPath, err := installer.GetProfileInstalledPath()
if err != nil {
return profileContexts, nil
}
fileProfileStore, err := installer.NewFileDirProfileStore(profileInstalledPath)
if err != nil {
return profileContexts, nil
}
fileProfileContexts, err := fileProfileStore.List()
if err == nil {
profileContexts = append(profileContexts, fileProfileContexts...)
}
cliClient, err := kubernetes.NewCLIClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
if err != nil {
return profileContexts, nil
}
configmapProfileStore, err := installer.NewConfigmapProfileStore(cliClient)
if err != nil {
return profileContexts, nil
}
configmapProfileContexts, err := configmapProfileStore.List()
if err == nil {
profileContexts = append(profileContexts, configmapProfileContexts...)
}
return profileContexts, nil
}
func promptProfileContexts(writer io.Writer, profileContexts []*installer.ProfileContext) *installer.ProfileContext {
if len(profileContexts) == 1 {
fmt.Fprintf(writer, "\nFound a profile:: ")
} else {
fmt.Fprintf(writer, "\nPlease select higress installed configration profiles:\n")
}
index := 1
for _, profileContext := range profileContexts {
if len(profileContexts) > 1 {
fmt.Fprintf(writer, "\n%d: ", index)
}
fmt.Fprintf(writer, "install mode: %s, profile location: %s", profileContext.Install, profileContext.PathOrName)
if len(profileContext.Namespace) > 0 {
fmt.Fprintf(writer, ", namespace: %s", profileContext.Namespace)
}
if len(profileContext.HigressVersion) > 0 {
fmt.Fprintf(writer, ", version: %s", profileContext.HigressVersion)
}
fmt.Fprintf(writer, "\n")
index++
}
if len(profileContexts) == 1 {
return profileContexts[0]
}
answer := ""
for {
fmt.Fprintf(writer, "\nPlease input 1 to %d select, input your selection:", len(profileContexts))
fmt.Scanln(&answer)
index, err := strconv.Atoi(answer)
if err == nil && index >= 1 && index <= len(profileContexts) {
return profileContexts[index-1]
}
}
}

View File

@@ -140,8 +140,6 @@ type IngressConfig struct {
annotationHandler annotations.AnnotationHandler
globalGatewayName string
namespace string
clusterId string
@@ -157,13 +155,11 @@ func NewIngressConfig(localKubeClient kube.Client, XDSUpdater model.XDSUpdater,
XDSUpdater: XDSUpdater,
annotationHandler: annotations.NewAnnotationHandlerManager(),
clusterId: clusterId,
globalGatewayName: namespace + "/" +
common.CreateConvertedName(clusterId, "global"),
watchedSecretSet: sets.NewSet(),
namespace: namespace,
mcpbridgeReconciled: atomic.NewBool(false),
wasmPlugins: make(map[string]*extensions.WasmPlugin),
http2rpcs: make(map[string]*higressv1.Http2Rpc),
watchedSecretSet: sets.NewSet(),
namespace: namespace,
mcpbridgeReconciled: atomic.NewBool(false),
wasmPlugins: make(map[string]*extensions.WasmPlugin),
http2rpcs: make(map[string]*higressv1.Http2Rpc),
}
mcpbridgeController := mcpbridge.NewController(localKubeClient, clusterId)
mcpbridgeController.AddEventHandler(config.AddOrUpdateMcpBridge, config.DeleteMcpBridge)
@@ -479,7 +475,7 @@ func (m *IngressConfig) convertVirtualService(configs []common.WrapperConfig) []
common.CreateConvertedName(m.clusterId, cleanHost),
common.CreateConvertedName(constants.IstioIngressGatewayName, cleanHost)}
if host != "*" {
gateways = append(gateways, m.globalGatewayName)
gateways = append(gateways, m.namespace+"/"+common.CreateConvertedName(m.clusterId, common.CleanHost("*")))
}
wrapperVS, exist := convertOptions.VirtualServices[host]
@@ -530,7 +526,7 @@ func (m *IngressConfig) convertEnvoyFilter(convertOptions *common.ConvertOptions
IngressLog.Infof("Found http2rpc for name %s", http2rpc.Name)
envoyFilter, err := m.constructHttp2RpcEnvoyFilter(http2rpc, route, m.namespace)
if err != nil {
IngressLog.Errorf("Construct http2rpc EnvoyFilter error %v", err)
IngressLog.Infof("Construct http2rpc EnvoyFilter error %v", err)
} else {
IngressLog.Infof("Append http2rpc EnvoyFilter for name %s", http2rpc.Name)
envoyFilters = append(envoyFilters, *envoyFilter)
@@ -573,6 +569,7 @@ func (m *IngressConfig) convertEnvoyFilter(convertOptions *common.ConvertOptions
// TODO Support other envoy filters
IngressLog.Infof("Found %d number of envoyFilters", len(envoyFilters))
m.mutex.Lock()
m.cachedEnvoyFilters = envoyFilters
m.mutex.Unlock()
@@ -1003,9 +1000,23 @@ func (m *IngressConfig) AddOrUpdateHttp2Rpc(clusterNamespacedName util.ClusterNa
m.http2rpcs[clusterNamespacedName.Name] = &http2rpc.Spec
m.mutex.Unlock()
IngressLog.Infof("AddOrUpdateHttp2Rpc http2rpc ingress name %s", clusterNamespacedName.Name)
push := func(kind config.GroupVersionKind) {
m.XDSUpdater.ConfigUpdate(&model.PushRequest{
Full: true,
ConfigsUpdated: map[model.ConfigKey]struct{}{{
Kind: kind,
Name: clusterNamespacedName.Name,
Namespace: clusterNamespacedName.Namespace,
}: {}},
Reason: []model.TriggerReason{"Http2Rpc-AddOrUpdate"},
})
}
push(gvk.VirtualService)
push(gvk.EnvoyFilter)
}
func (m *IngressConfig) DeleteHttp2Rpc(clusterNamespacedName util.ClusterNamespacedName) {
IngressLog.Infof("Http2Rpc triggerd deleted event %s", clusterNamespacedName.Name)
if clusterNamespacedName.Namespace != m.namespace {
return
}
@@ -1017,7 +1028,20 @@ func (m *IngressConfig) DeleteHttp2Rpc(clusterNamespacedName util.ClusterNamespa
}
m.mutex.Unlock()
if hit {
IngressLog.Debugf("Http2Rpc triggerd deleted %s", clusterNamespacedName.Name)
IngressLog.Infof("Http2Rpc triggerd deleted event executed %s", clusterNamespacedName.Name)
push := func(kind config.GroupVersionKind) {
m.XDSUpdater.ConfigUpdate(&model.PushRequest{
Full: true,
ConfigsUpdated: map[model.ConfigKey]struct{}{{
Kind: kind,
Name: clusterNamespacedName.Name,
Namespace: clusterNamespacedName.Namespace,
}: {}},
Reason: []model.TriggerReason{"Http2Rpc-Deleted"},
})
}
push(gvk.VirtualService)
push(gvk.EnvoyFilter)
}
}
@@ -1241,12 +1265,21 @@ func (m *IngressConfig) constructHttp2RpcMethods(dubbo *higressv1.DubboService)
var method = make(map[string]interface{})
method["name"] = serviceMethod.GetServiceMethod()
var params []interface{}
for _, methodParam := range serviceMethod.GetParams() {
// paramFromEntireBody is for methods with single parameter. So when paramFromEntireBody exists, we just ignore parmas.
var paramFromEntireBody = serviceMethod.GetParamFromEntireBody()
if paramFromEntireBody != nil {
var param = make(map[string]interface{})
param["extract_key"] = methodParam.GetParamKey()
param["extract_key_spec"] = Http2RpcParamSourceMap()[methodParam.GetParamSource()]
param["mapping_type"] = methodParam.GetParamType()
param["extract_key_spec"] = Http2RpcParamSourceMap()["BODY"]
param["mapping_type"] = paramFromEntireBody.GetParamType()
params = append(params, param)
} else {
for _, methodParam := range serviceMethod.GetParams() {
var param = make(map[string]interface{})
param["extract_key"] = methodParam.GetParamKey()
param["extract_key_spec"] = Http2RpcParamSourceMap()[methodParam.GetParamSource()]
param["mapping_type"] = methodParam.GetParamType()
params = append(params, param)
}
}
method["parameter_mapping"] = params
var path_matcher = make(map[string]interface{})

View File

@@ -257,7 +257,7 @@ func TestConvertGatewaysForIngress(t *testing.T) {
"foo.com": {
Meta: config.Meta{
GroupVersionKind: gvk.Gateway,
Name: "istio-autogenerated-k8s-ingress-foo-com",
Name: "istio-autogenerated-k8s-ingress-" + common.CleanHost("foo.com"),
Namespace: "wakanda",
Annotations: map[string]string{
common.ClusterIdAnnotation: "ingress-v1beta1",
@@ -270,7 +270,7 @@ func TestConvertGatewaysForIngress(t *testing.T) {
Port: &networking.Port{
Number: 80,
Protocol: "HTTP",
Name: "http-80-ingress-ingress-v1beta1-wakanda-test-1-foo-com",
Name: "http-80-ingress-ingress-v1beta1",
},
Hosts: []string{"foo.com"},
},
@@ -278,7 +278,7 @@ func TestConvertGatewaysForIngress(t *testing.T) {
Port: &networking.Port{
Number: 443,
Protocol: "HTTPS",
Name: "https-443-ingress-ingress-v1beta1-wakanda-test-2-foo-com",
Name: "https-443-ingress-ingress-v1beta1",
},
Hosts: []string{"foo.com"},
Tls: &networking.ServerTLSSettings{
@@ -293,7 +293,7 @@ func TestConvertGatewaysForIngress(t *testing.T) {
"test.com": {
Meta: config.Meta{
GroupVersionKind: gvk.Gateway,
Name: "istio-autogenerated-k8s-ingress-test-com",
Name: "istio-autogenerated-k8s-ingress-" + common.CleanHost("test.com"),
Namespace: "wakanda",
Annotations: map[string]string{
common.ClusterIdAnnotation: "ingress-v1beta1",
@@ -306,7 +306,7 @@ func TestConvertGatewaysForIngress(t *testing.T) {
Port: &networking.Port{
Number: 80,
Protocol: "HTTP",
Name: "http-80-ingress-ingress-v1beta1-wakanda-test-1-test-com",
Name: "http-80-ingress-ingress-v1beta1",
},
Hosts: []string{"test.com"},
},
@@ -314,7 +314,7 @@ func TestConvertGatewaysForIngress(t *testing.T) {
Port: &networking.Port{
Number: 443,
Protocol: "HTTPS",
Name: "https-443-ingress-ingress-v1beta1-wakanda-test-1-test-com",
Name: "https-443-ingress-ingress-v1beta1",
},
Hosts: []string{"test.com"},
Tls: &networking.ServerTLSSettings{
@@ -329,7 +329,7 @@ func TestConvertGatewaysForIngress(t *testing.T) {
"bar.com": {
Meta: config.Meta{
GroupVersionKind: gvk.Gateway,
Name: "istio-autogenerated-k8s-ingress-bar-com",
Name: "istio-autogenerated-k8s-ingress-" + common.CleanHost("bar.com"),
Namespace: "wakanda",
Annotations: map[string]string{
common.ClusterIdAnnotation: "ingress-v1beta1",
@@ -342,7 +342,7 @@ func TestConvertGatewaysForIngress(t *testing.T) {
Port: &networking.Port{
Number: 80,
Protocol: "HTTP",
Name: "http-80-ingress-ingress-v1beta1-wakanda-test-2-bar-com",
Name: "http-80-ingress-ingress-v1beta1",
},
Hosts: []string{"bar.com"},
},
@@ -471,7 +471,7 @@ func TestConvertGatewaysForIngress(t *testing.T) {
"foo.com": {
Meta: config.Meta{
GroupVersionKind: gvk.Gateway,
Name: "istio-autogenerated-k8s-ingress-foo-com",
Name: "istio-autogenerated-k8s-ingress-" + common.CleanHost("foo.com"),
Namespace: "wakanda",
Annotations: map[string]string{
common.ClusterIdAnnotation: "ingress-v1",
@@ -484,7 +484,7 @@ func TestConvertGatewaysForIngress(t *testing.T) {
Port: &networking.Port{
Number: 80,
Protocol: "HTTP",
Name: "http-80-ingress-ingress-v1-wakanda-test-1-foo-com",
Name: "http-80-ingress-ingress-v1",
},
Hosts: []string{"foo.com"},
},
@@ -492,7 +492,7 @@ func TestConvertGatewaysForIngress(t *testing.T) {
Port: &networking.Port{
Number: 443,
Protocol: "HTTPS",
Name: "https-443-ingress-ingress-v1-wakanda-test-2-foo-com",
Name: "https-443-ingress-ingress-v1",
},
Hosts: []string{"foo.com"},
Tls: &networking.ServerTLSSettings{
@@ -507,7 +507,7 @@ func TestConvertGatewaysForIngress(t *testing.T) {
"test.com": {
Meta: config.Meta{
GroupVersionKind: gvk.Gateway,
Name: "istio-autogenerated-k8s-ingress-test-com",
Name: "istio-autogenerated-k8s-ingress-" + common.CleanHost("test.com"),
Namespace: "wakanda",
Annotations: map[string]string{
common.ClusterIdAnnotation: "ingress-v1",
@@ -520,7 +520,7 @@ func TestConvertGatewaysForIngress(t *testing.T) {
Port: &networking.Port{
Number: 80,
Protocol: "HTTP",
Name: "http-80-ingress-ingress-v1-wakanda-test-1-test-com",
Name: "http-80-ingress-ingress-v1",
},
Hosts: []string{"test.com"},
},
@@ -528,7 +528,7 @@ func TestConvertGatewaysForIngress(t *testing.T) {
Port: &networking.Port{
Number: 443,
Protocol: "HTTPS",
Name: "https-443-ingress-ingress-v1-wakanda-test-1-test-com",
Name: "https-443-ingress-ingress-v1",
},
Hosts: []string{"test.com"},
Tls: &networking.ServerTLSSettings{
@@ -543,7 +543,7 @@ func TestConvertGatewaysForIngress(t *testing.T) {
"bar.com": {
Meta: config.Meta{
GroupVersionKind: gvk.Gateway,
Name: "istio-autogenerated-k8s-ingress-bar-com",
Name: "istio-autogenerated-k8s-ingress-" + common.CleanHost("bar.com"),
Namespace: "wakanda",
Annotations: map[string]string{
common.ClusterIdAnnotation: "ingress-v1",
@@ -556,7 +556,7 @@ func TestConvertGatewaysForIngress(t *testing.T) {
Port: &networking.Port{
Number: 80,
Protocol: "HTTP",
Name: "http-80-ingress-ingress-v1-wakanda-test-2-bar-com",
Name: "http-80-ingress-ingress-v1",
},
Hosts: []string{"bar.com"},
},

View File

@@ -66,8 +66,6 @@ type KIngressConfig struct {
annotationHandler annotations.AnnotationHandler
globalGatewayName string
namespace string
clusterId string
@@ -86,10 +84,8 @@ func NewKIngressConfig(localKubeClient kube.Client, XDSUpdater model.XDSUpdater,
XDSUpdater: XDSUpdater,
annotationHandler: annotations.NewAnnotationHandlerManager(),
clusterId: clusterId,
globalGatewayName: namespace + "/" +
common.CreateConvertedName(clusterId, "global"),
watchedSecretSet: sets.NewSet(),
namespace: namespace,
watchedSecretSet: sets.NewSet(),
namespace: namespace,
}
return config
@@ -319,7 +315,7 @@ func (m *KIngressConfig) convertVirtualService(configs []common.WrapperConfig) [
common.CreateConvertedName(m.clusterId, cleanHost),
common.CreateConvertedName(constants.IstioIngressGatewayName, cleanHost)}
if host != "*" {
gateways = append(gateways, m.globalGatewayName)
gateways = append(gateways, m.namespace+"/"+common.CreateConvertedName(m.clusterId, common.CleanHost("*")))
}
wrapperVS, exist := convertOptions.VirtualServices[host]

View File

@@ -363,7 +363,7 @@ func TestConvertGatewaysForKIngress(t *testing.T) {
"foo.com": {
Meta: config.Meta{
GroupVersionKind: gvk.Gateway,
Name: "istio-autogenerated-k8s-ingress-foo-com",
Name: "istio-autogenerated-k8s-ingress-" + common.CleanHost("foo.com"),
Namespace: "wakanda",
Annotations: map[string]string{
common.ClusterIdAnnotation: "kingress",
@@ -376,7 +376,7 @@ func TestConvertGatewaysForKIngress(t *testing.T) {
Port: &networking.Port{
Number: 80,
Protocol: "HTTP",
Name: "http-80-ingress-kingress-wakanda-test-1-foo-com",
Name: "http-80-ingress-kingress",
},
Hosts: []string{"foo.com"},
//Tls: &networking.ServerTLSSettings{
@@ -387,7 +387,7 @@ func TestConvertGatewaysForKIngress(t *testing.T) {
Port: &networking.Port{
Number: 443,
Protocol: "HTTPS",
Name: "https-443-ingress-kingress-wakanda-test-2-foo-com",
Name: "https-443-ingress-kingress",
},
Hosts: []string{"foo.com"},
Tls: &networking.ServerTLSSettings{
@@ -402,7 +402,7 @@ func TestConvertGatewaysForKIngress(t *testing.T) {
"test.com": {
Meta: config.Meta{
GroupVersionKind: gvk.Gateway,
Name: "istio-autogenerated-k8s-ingress-test-com",
Name: "istio-autogenerated-k8s-ingress-" + common.CleanHost("test.com"),
Namespace: "wakanda",
Annotations: map[string]string{
common.ClusterIdAnnotation: "kingress",
@@ -415,7 +415,7 @@ func TestConvertGatewaysForKIngress(t *testing.T) {
Port: &networking.Port{
Number: 80,
Protocol: "HTTP",
Name: "http-80-ingress-kingress-wakanda-test-1-test-com",
Name: "http-80-ingress-kingress",
},
Hosts: []string{"test.com"},
//Tls: &networking.ServerTLSSettings{
@@ -426,7 +426,7 @@ func TestConvertGatewaysForKIngress(t *testing.T) {
Port: &networking.Port{
Number: 443,
Protocol: "HTTPS",
Name: "https-443-ingress-kingress-wakanda-test-1-test-com",
Name: "https-443-ingress-kingress",
},
Hosts: []string{"test.com"},
Tls: &networking.ServerTLSSettings{
@@ -441,7 +441,7 @@ func TestConvertGatewaysForKIngress(t *testing.T) {
"bar.com": {
Meta: config.Meta{
GroupVersionKind: gvk.Gateway,
Name: "istio-autogenerated-k8s-ingress-bar-com",
Name: "istio-autogenerated-k8s-ingress-" + common.CleanHost("bar.com"),
Namespace: "wakanda",
Annotations: map[string]string{
common.ClusterIdAnnotation: "kingress",
@@ -454,7 +454,7 @@ func TestConvertGatewaysForKIngress(t *testing.T) {
Port: &networking.Port{
Number: 80,
Protocol: "HTTP",
Name: "http-80-ingress-kingress-wakanda-test-2-bar-com",
Name: "http-80-ingress-kingress",
},
Hosts: []string{"bar.com"},
},

View File

@@ -15,6 +15,8 @@
package annotations
import (
"strings"
networking "istio.io/api/networking/v1alpha3"
"istio.io/istio/pilot/pkg/util/sets"
listersv1 "k8s.io/client-go/listers/core/v1"
@@ -54,10 +56,14 @@ type Ingress struct {
IPAccessControl *IPAccessControlConfig
Timeout *TimeoutConfig
Retry *RetryConfig
LoadBalance *LoadBalanceConfig
localRateLimit *localRateLimitConfig
Fallback *FallbackConfig
Auth *AuthConfig
@@ -73,12 +79,17 @@ type Ingress struct {
Http2Rpc *Http2RpcConfig
}
func (i *Ingress) NeedRegexMatch() bool {
func (i *Ingress) NeedRegexMatch(path string) bool {
if i.Rewrite == nil {
return false
}
return i.Rewrite.RewriteTarget != "" || i.IsPrefixRegexMatch() || i.IsFullPathRegexMatch()
if strings.ContainsAny(path, `\.+*?()|[]{}^$`) {
return true
}
if strings.ContainsAny(i.Rewrite.RewriteTarget, `$\`) {
return true
}
return i.IsPrefixRegexMatch() || i.IsFullPathRegexMatch()
}
func (i *Ingress) IsPrefixRegexMatch() bool {
@@ -143,8 +154,10 @@ func NewAnnotationHandlerManager() AnnotationHandler {
rewrite{},
upstreamTLS{},
ipAccessControl{},
timeout{},
retry{},
loadBalance{},
localRateLimit{},
fallback{},
auth{},
destination{},
@@ -164,7 +177,9 @@ func NewAnnotationHandlerManager() AnnotationHandler {
redirect{},
rewrite{},
ipAccessControl{},
timeout{},
retry{},
localRateLimit{},
fallback{},
ignoreCaseMatching{},
match{},

View File

@@ -18,8 +18,9 @@ import "testing"
func TestNeedRegexMatch(t *testing.T) {
testCases := []struct {
input *Ingress
expect bool
input *Ingress
inputPath string
expect bool
}{
{
input: &Ingress{},
@@ -34,7 +35,7 @@ func TestNeedRegexMatch(t *testing.T) {
{
input: &Ingress{
Rewrite: &RewriteConfig{
RewriteTarget: "/test",
UseRegex: true,
},
},
expect: true,
@@ -42,17 +43,46 @@ func TestNeedRegexMatch(t *testing.T) {
{
input: &Ingress{
Rewrite: &RewriteConfig{
UseRegex: true,
UseRegex: false,
},
},
expect: false,
},
{
input: &Ingress{
Rewrite: &RewriteConfig{
UseRegex: false,
RewriteTarget: "/$1",
},
},
expect: true,
},
{
input: &Ingress{
Rewrite: &RewriteConfig{
UseRegex: false,
RewriteTarget: "/",
},
},
inputPath: "/.*",
expect: true,
},
{
input: &Ingress{
Rewrite: &RewriteConfig{
UseRegex: false,
RewriteTarget: "/",
},
},
inputPath: "/",
expect: false,
},
}
for _, testCase := range testCases {
t.Run("", func(t *testing.T) {
if testCase.input.NeedRegexMatch() != testCase.expect {
t.Fatalf("Should be %t, but actual is %t", testCase.expect, testCase.input.NeedRegexMatch())
if testCase.input.NeedRegexMatch(testCase.inputPath) != testCase.expect {
t.Fatalf("Should be %t, but actual is %t", testCase.expect, !testCase.expect)
}
})
}

View File

@@ -108,6 +108,8 @@ func ApplyByWeight(canary, route *networking.HTTPRoute, canaryIngress *Ingress)
// canary route use the header control applied on itself.
headerControl{}.ApplyRoute(canary, canaryIngress)
// reset
canary.Route[0].FallbackClusters = nil
// Move route level to destination level
canary.Route[0].Headers = canary.Headers
@@ -127,8 +129,6 @@ func ApplyByHeader(canary, route *networking.HTTPRoute, canaryIngress *Ingress)
// Inherit configuration from non-canary rule
route.DeepCopyInto(canary)
// Assign temp copied canary route match
canary.Match = temp.Match
// Assign temp copied canary route destination
canary.Route = temp.Route
@@ -154,7 +154,7 @@ func ApplyByHeader(canary, route *networking.HTTPRoute, canaryIngress *Ingress)
match.Headers = map[string]*networking.StringMatch{
canaryConfig.Header: {
MatchType: &networking.StringMatch_Regex{
Regex: canaryConfig.HeaderPattern,
Regex: ".*" + canaryConfig.HeaderPattern + ".*",
},
},
}
@@ -165,7 +165,7 @@ func ApplyByHeader(canary, route *networking.HTTPRoute, canaryIngress *Ingress)
match.Headers = map[string]*networking.StringMatch{
"cookie": {
MatchType: &networking.StringMatch_Regex{
Regex: "^(.\\*?;)?(" + canaryConfig.Cookie + "=always)(;.\\*)?$",
Regex: "^(.*?;\\s*)?(" + canaryConfig.Cookie + "=always)(;.*)?$",
},
},
}

View File

@@ -15,6 +15,7 @@
package annotations
import (
"regexp"
"strings"
networking "istio.io/api/networking/v1alpha3"
@@ -37,6 +38,8 @@ const (
var (
_ Parser = headerControl{}
_ RouteHandler = headerControl{}
pattern = regexp.MustCompile(`\s+`)
)
type HeaderOperation struct {
@@ -138,6 +141,18 @@ func needHeaderControlConfig(annotations Annotations) bool {
annotations.HasHigress(responseHeaderRemove)
}
func trimQuotes(s string) string {
if len(s) >= 2 {
if s[0] == '"' && s[len(s)-1] == '"' {
return s[1 : len(s)-1]
}
if s[0] == '\'' && s[len(s)-1] == '\'' {
return s[1 : len(s)-1]
}
}
return s
}
func convertAddOrUpdate(headers string) map[string]string {
result := map[string]string{}
parts := strings.Split(headers, "\n")
@@ -147,13 +162,13 @@ func convertAddOrUpdate(headers string) map[string]string {
continue
}
keyValue := strings.Fields(part)
keyValue := pattern.Split(part, 2)
if len(keyValue) != 2 {
IngressLog.Errorf("Header format %s is invalid.", keyValue)
continue
}
key := strings.TrimSpace(keyValue[0])
value := strings.TrimSpace(keyValue[1])
key := trimQuotes(strings.TrimSpace(keyValue[0]))
value := trimQuotes(strings.TrimSpace(keyValue[1]))
result[key] = value
}
return result

View File

@@ -48,8 +48,8 @@ func TestHeaderControlParse(t *testing.T) {
},
{
input: map[string]string{
buildHigressAnnotationKey(requestHeaderAdd): "one 1\n two 2\nthree 3 \n",
buildHigressAnnotationKey(requestHeaderUpdate): "two 2",
buildHigressAnnotationKey(requestHeaderAdd): "one 1\n two 2\nthree 3 \nx-test mse; test=true\nx-pro mse; pro=true\n",
buildHigressAnnotationKey(requestHeaderUpdate): "two 2\n set-cookie name=test; sameage=111\nset-stage name=stage; stage=true\n",
buildHigressAnnotationKey(requestHeaderRemove): "one, two,three\n",
buildHigressAnnotationKey(responseHeaderAdd): "A a\nB b\n",
buildHigressAnnotationKey(responseHeaderUpdate): "X x\nY y\n",
@@ -58,12 +58,16 @@ func TestHeaderControlParse(t *testing.T) {
expect: &HeaderControlConfig{
Request: &HeaderOperation{
Add: map[string]string{
"one": "1",
"two": "2",
"three": "3",
"one": "1",
"two": "2",
"three": "3",
"x-test": "mse; test=true",
"x-pro": "mse; pro=true",
},
Update: map[string]string{
"two": "2",
"two": "2",
"set-cookie": "name=test; sameage=111",
"set-stage": "name=stage; stage=true",
},
Remove: []string{"one", "two", "three"},
},
@@ -122,12 +126,16 @@ func TestHeaderControlApplyRoute(t *testing.T) {
HeaderControl: &HeaderControlConfig{
Request: &HeaderOperation{
Add: map[string]string{
"one": "1",
"two": "2",
"three": "3",
"one": "1",
"two": "2",
"three": "3",
"x-test": "mse; test=true",
"x-pro": "mse; pro=true",
},
Update: map[string]string{
"two": "2",
"two": "2",
"set-cookie": "name=test; sameage=111",
"set-stage": "name=stage; sameage=111",
},
Remove: []string{"one", "two", "three"},
},
@@ -138,12 +146,16 @@ func TestHeaderControlApplyRoute(t *testing.T) {
Headers: &networking.Headers{
Request: &networking.Headers_HeaderOperations{
Add: map[string]string{
"one": "1",
"two": "2",
"three": "3",
"one": "1",
"two": "2",
"three": "3",
"x-test": "mse; test=true",
"x-pro": "mse; pro=true",
},
Set: map[string]string{
"two": "2",
"two": "2",
"set-cookie": "name=test; sameage=111",
"set-stage": "name=stage; sameage=111",
},
Remove: []string{"one", "two", "three"},
},

View File

@@ -0,0 +1,110 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// 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 annotations
import (
types "github.com/gogo/protobuf/types"
networking "istio.io/api/networking/v1alpha3"
"istio.io/istio/pilot/pkg/networking/core/v1alpha3/mseingress"
)
const (
limitRPM = "route-limit-rpm"
limitRPS = "route-limit-rps"
limitBurstMultiplier = "route-limit-burst-multiplier"
defaultBurstMultiplier = 5
defaultStatusCode = 429
)
var (
_ Parser = localRateLimit{}
_ RouteHandler = localRateLimit{}
second = &types.Duration{
Seconds: 1,
}
minute = &types.Duration{
Seconds: 60,
}
)
type localRateLimitConfig struct {
TokensPerFill uint32
MaxTokens uint32
FillInterval *types.Duration
}
type localRateLimit struct{}
func (l localRateLimit) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
if !needLocalRateLimitConfig(annotations) {
return nil
}
var local *localRateLimitConfig
defer func() {
config.localRateLimit = local
}()
multiplier := defaultBurstMultiplier
if m, err := annotations.ParseIntForHigress(limitBurstMultiplier); err == nil {
multiplier = m
}
if rpm, err := annotations.ParseIntForHigress(limitRPM); err == nil {
local = &localRateLimitConfig{
MaxTokens: uint32(rpm * multiplier),
TokensPerFill: uint32(rpm),
FillInterval: minute,
}
} else if rps, err := annotations.ParseIntForHigress(limitRPS); err == nil {
local = &localRateLimitConfig{
MaxTokens: uint32(rps * multiplier),
TokensPerFill: uint32(rps),
FillInterval: second,
}
}
return nil
}
func (l localRateLimit) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
localRateLimitConfig := config.localRateLimit
if localRateLimitConfig == nil {
return
}
route.RouteHTTPFilters = append(route.RouteHTTPFilters, &networking.HTTPFilter{
Name: mseingress.LocalRateLimit,
Filter: &networking.HTTPFilter_LocalRateLimit{
LocalRateLimit: &networking.LocalRateLimit{
TokenBucket: &networking.TokenBucket{
MaxTokens: localRateLimitConfig.MaxTokens,
TokensPefFill: localRateLimitConfig.TokensPerFill,
FillInterval: localRateLimitConfig.FillInterval,
},
StatusCode: defaultStatusCode,
},
},
})
}
func needLocalRateLimitConfig(annotations Annotations) bool {
return annotations.HasHigress(limitRPM) ||
annotations.HasHigress(limitRPS)
}

View File

@@ -0,0 +1,127 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// 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 annotations
import (
"reflect"
"testing"
networking "istio.io/api/networking/v1alpha3"
"istio.io/istio/pilot/pkg/networking/core/v1alpha3/mseingress"
)
func TestLocalRateLimitParse(t *testing.T) {
localRateLimit := localRateLimit{}
inputCases := []struct {
input map[string]string
expect *localRateLimitConfig
}{
{},
{
input: map[string]string{
buildHigressAnnotationKey(limitRPM): "2",
},
expect: &localRateLimitConfig{
MaxTokens: 10,
TokensPerFill: 2,
FillInterval: minute,
},
},
{
input: map[string]string{
buildHigressAnnotationKey(limitRPM): "2",
buildHigressAnnotationKey(limitRPS): "3",
buildHigressAnnotationKey(limitBurstMultiplier): "10",
},
expect: &localRateLimitConfig{
MaxTokens: 20,
TokensPerFill: 2,
FillInterval: minute,
},
},
{
input: map[string]string{
buildHigressAnnotationKey(limitRPS): "3",
buildHigressAnnotationKey(limitBurstMultiplier): "10",
},
expect: &localRateLimitConfig{
MaxTokens: 30,
TokensPerFill: 3,
FillInterval: second,
},
},
}
for _, inputCase := range inputCases {
t.Run("", func(t *testing.T) {
config := &Ingress{}
_ = localRateLimit.Parse(inputCase.input, config, nil)
if !reflect.DeepEqual(inputCase.expect, config.localRateLimit) {
t.Fatal("Should be equal")
}
})
}
}
func TestLocalRateLimitApplyRoute(t *testing.T) {
localRateLimit := localRateLimit{}
inputCases := []struct {
config *Ingress
input *networking.HTTPRoute
expect *networking.HTTPRoute
}{
{
config: &Ingress{},
input: &networking.HTTPRoute{},
expect: &networking.HTTPRoute{},
},
{
config: &Ingress{
localRateLimit: &localRateLimitConfig{
MaxTokens: 60,
TokensPerFill: 20,
FillInterval: second,
},
},
input: &networking.HTTPRoute{},
expect: &networking.HTTPRoute{
RouteHTTPFilters: []*networking.HTTPFilter{
{
Name: mseingress.LocalRateLimit,
Filter: &networking.HTTPFilter_LocalRateLimit{
LocalRateLimit: &networking.LocalRateLimit{
TokenBucket: &networking.TokenBucket{
MaxTokens: 60,
TokensPefFill: 20,
FillInterval: second,
},
StatusCode: defaultStatusCode,
},
},
},
},
},
},
}
for _, inputCase := range inputCases {
t.Run("", func(t *testing.T) {
localRateLimit.ApplyRoute(inputCase.input, inputCase.config)
if !reflect.DeepEqual(inputCase.input, inputCase.expect) {
t.Fatal("Should be equal")
}
})
}
}

View File

@@ -70,8 +70,13 @@ func (r retry) Parse(annotations Annotations, config *Ingress, _ *GlobalContext)
}
if retryOn, err := annotations.ParseStringASAP(retryOn); err == nil {
extraConfigs := splitBySeparator(retryOn, ",")
conditions := toSet(extraConfigs)
var retryOnConditions []string
if strings.Contains(retryOn, ",") {
retryOnConditions = splitBySeparator(retryOn, ",")
} else {
retryOnConditions = strings.Fields(retryOn)
}
conditions := toSet(retryOnConditions)
if len(conditions) > 0 {
if conditions.Contains("off") {
retryConfig.retryCount = 0
@@ -88,7 +93,7 @@ func (r retry) Parse(annotations Annotations, config *Ingress, _ *GlobalContext)
stringBuilder.WriteString("non_idempotent,")
}
// Append the status codes.
statusCodes := convertStatusCodes(extraConfigs)
statusCodes := convertStatusCodes(retryOnConditions)
if len(statusCodes) > 0 {
stringBuilder.WriteString(retryStatusCode + ",")
for _, code := range statusCodes {

View File

@@ -37,8 +37,8 @@ func TestRetryParse(t *testing.T) {
},
expect: &RetryConfig{
retryCount: 1,
perRetryTimeout: &types.Duration{},
retryOn: "5xx",
perRetryTimeout: &types.Duration{},
},
},
{
@@ -60,8 +60,8 @@ func TestRetryParse(t *testing.T) {
},
expect: &RetryConfig{
retryCount: 0,
perRetryTimeout: &types.Duration{},
retryOn: "5xx",
perRetryTimeout: &types.Duration{},
},
},
{
@@ -71,8 +71,19 @@ func TestRetryParse(t *testing.T) {
},
expect: &RetryConfig{
retryCount: 2,
perRetryTimeout: &types.Duration{},
retryOn: "5xx",
perRetryTimeout: &types.Duration{},
},
},
{
input: map[string]string{
buildNginxAnnotationKey(retryCount): "2",
buildNginxAnnotationKey(retryOn): "error timeout",
},
expect: &RetryConfig{
retryCount: 2,
retryOn: "5xx",
perRetryTimeout: &types.Duration{},
},
},
{
@@ -81,8 +92,18 @@ func TestRetryParse(t *testing.T) {
},
expect: &RetryConfig{
retryCount: 3,
perRetryTimeout: &types.Duration{},
retryOn: "5xx,non_idempotent",
perRetryTimeout: &types.Duration{},
},
},
{
input: map[string]string{
buildNginxAnnotationKey(retryOn): "timeout non_idempotent",
},
expect: &RetryConfig{
retryCount: 3,
retryOn: "5xx,non_idempotent",
perRetryTimeout: &types.Duration{},
},
},
{
@@ -91,18 +112,18 @@ func TestRetryParse(t *testing.T) {
},
expect: &RetryConfig{
retryCount: 3,
perRetryTimeout: &types.Duration{},
retryOn: "5xx,retriable-status-codes,503,502,404",
perRetryTimeout: &types.Duration{},
},
},
{
input: map[string]string{
buildNginxAnnotationKey(retryOn): "timeout,http_505,http_503,http_502,http_404,http_403",
buildNginxAnnotationKey(retryOn): "timeout http_503 http_502 http_404",
},
expect: &RetryConfig{
retryCount: 3,
retryOn: "5xx,retriable-status-codes,503,502,404",
perRetryTimeout: &types.Duration{},
retryOn: "5xx,retriable-status-codes,505,503,502,404,403",
},
},
}

View File

@@ -59,12 +59,6 @@ func (r rewrite) Parse(annotations Annotations, config *Ingress, _ *GlobalContex
rewriteConfig.RewritePath, _ = annotations.ParseStringForHigress(rewritePath)
if rewriteConfig.RewritePath == "" && rewriteConfig.RewriteTarget != "" {
// When rewrite target is present and not empty,
// we will enforce regex match on all rules in this ingress.
if !rewriteConfig.UseRegex && !rewriteConfig.FullPathRegex {
rewriteConfig.UseRegex = true
}
// We should convert nginx regex rule to envoy regex rule.
rewriteConfig.RewriteTarget = convertToRE2(rewriteConfig.RewriteTarget)
}
@@ -92,9 +86,22 @@ func (r rewrite) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
}
}
} else if rewriteConfig.RewriteTarget != "" {
route.Rewrite.UriRegex = &networking.RegexMatchAndSubstitute{
Pattern: route.Match[0].Uri.GetRegex(),
Substitution: rewriteConfig.RewriteTarget,
uri := route.Match[0].Uri
if uri.GetExact() != "" {
route.Rewrite.UriRegex = &networking.RegexMatchAndSubstitute{
Pattern: uri.GetExact(),
Substitution: rewriteConfig.RewriteTarget,
}
} else if uri.GetPrefix() != "" {
route.Rewrite.UriRegex = &networking.RegexMatchAndSubstitute{
Pattern: uri.GetPrefix(),
Substitution: rewriteConfig.RewriteTarget,
}
} else {
route.Rewrite.UriRegex = &networking.RegexMatchAndSubstitute{
Pattern: uri.GetRegex(),
Substitution: rewriteConfig.RewriteTarget,
}
}
}

View File

@@ -77,16 +77,13 @@ func TestRewriteParse(t *testing.T) {
},
expect: &RewriteConfig{
RewriteTarget: "/test",
UseRegex: true,
},
},
{
input: Annotations{
buildNginxAnnotationKey(rewriteTarget): "",
},
expect: &RewriteConfig{
UseRegex: false,
},
expect: &RewriteConfig{},
},
{
input: Annotations{
@@ -94,7 +91,6 @@ func TestRewriteParse(t *testing.T) {
},
expect: &RewriteConfig{
RewriteTarget: "/\\2/\\1",
UseRegex: true,
},
},
{
@@ -113,6 +109,16 @@ func TestRewriteParse(t *testing.T) {
RewriteHost: "test.com",
},
},
{
input: Annotations{
buildNginxAnnotationKey(useRegex): "true",
buildNginxAnnotationKey(rewriteTarget): "/$1",
},
expect: &RewriteConfig{
UseRegex: true,
RewriteTarget: "/\\1",
},
},
{
input: Annotations{
buildNginxAnnotationKey(rewriteTarget): "/$2/$1",
@@ -120,7 +126,6 @@ func TestRewriteParse(t *testing.T) {
},
expect: &RewriteConfig{
RewriteTarget: "/\\2/\\1",
UseRegex: true,
RewriteHost: "test.com",
},
},
@@ -330,6 +335,76 @@ func TestRewriteApplyRoute(t *testing.T) {
},
},
},
{
config: &Ingress{
Rewrite: &RewriteConfig{
RewriteTarget: "/test",
},
},
input: &networking.HTTPRoute{
Match: []*networking.HTTPMatchRequest{
{
Uri: &networking.StringMatch{
MatchType: &networking.StringMatch_Exact{
Exact: "/exact",
},
},
},
},
},
expect: &networking.HTTPRoute{
Match: []*networking.HTTPMatchRequest{
{
Uri: &networking.StringMatch{
MatchType: &networking.StringMatch_Exact{
Exact: "/exact",
},
},
},
},
Rewrite: &networking.HTTPRewrite{
UriRegex: &networking.RegexMatchAndSubstitute{
Pattern: "/exact",
Substitution: "/test",
},
},
},
},
{
config: &Ingress{
Rewrite: &RewriteConfig{
RewriteTarget: "/test",
},
},
input: &networking.HTTPRoute{
Match: []*networking.HTTPMatchRequest{
{
Uri: &networking.StringMatch{
MatchType: &networking.StringMatch_Prefix{
Prefix: "/prefix",
},
},
},
},
},
expect: &networking.HTTPRoute{
Match: []*networking.HTTPMatchRequest{
{
Uri: &networking.StringMatch{
MatchType: &networking.StringMatch_Prefix{
Prefix: "/prefix",
},
},
},
},
Rewrite: &networking.HTTPRewrite{
UriRegex: &networking.RegexMatchAndSubstitute{
Pattern: "/prefix",
Substitution: "/test",
},
},
},
},
}
for _, inputCase := range inputCases {

View File

@@ -0,0 +1,62 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// 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 annotations
import (
types "github.com/gogo/protobuf/types"
networking "istio.io/api/networking/v1alpha3"
)
const timeoutAnnotation = "timeout"
var (
_ Parser = timeout{}
_ RouteHandler = timeout{}
)
type TimeoutConfig struct {
time *types.Duration
}
type timeout struct{}
func (t timeout) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
if !needTimeoutConfig(annotations) {
return nil
}
if time, err := annotations.ParseIntForHigress(timeoutAnnotation); err == nil {
config.Timeout = &TimeoutConfig{
time: &types.Duration{
Seconds: int64(time),
},
}
}
return nil
}
func (t timeout) ApplyRoute(route *networking.HTTPRoute, config *Ingress) {
timeout := config.Timeout
if timeout == nil || timeout.time == nil || timeout.time.Seconds == 0 {
return
}
route.Timeout = timeout.time
}
func needTimeoutConfig(annotations Annotations) bool {
return annotations.HasHigress(timeoutAnnotation)
}

View File

@@ -0,0 +1,122 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// 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 annotations
import (
"reflect"
"testing"
types "github.com/gogo/protobuf/types"
networking "istio.io/api/networking/v1alpha3"
)
func TestTimeoutParse(t *testing.T) {
timeout := timeout{}
inputCases := []struct {
input map[string]string
expect *TimeoutConfig
}{
{},
{
input: map[string]string{
HigressAnnotationsPrefix + "/" + timeoutAnnotation: "",
},
},
{
input: map[string]string{
HigressAnnotationsPrefix + "/" + timeoutAnnotation: "0",
},
expect: &TimeoutConfig{
time: &types.Duration{},
},
},
{
input: map[string]string{
HigressAnnotationsPrefix + "/" + timeoutAnnotation: "10",
},
expect: &TimeoutConfig{
time: &types.Duration{
Seconds: 10,
},
},
},
}
for _, c := range inputCases {
t.Run("", func(t *testing.T) {
config := &Ingress{}
_ = timeout.Parse(c.input, config, nil)
if !reflect.DeepEqual(c.expect, config.Timeout) {
t.Fatalf("Should be equal.")
}
})
}
}
func TestTimeoutApplyRoute(t *testing.T) {
timeout := timeout{}
inputCases := []struct {
config *Ingress
input *networking.HTTPRoute
expect *networking.HTTPRoute
}{
{
config: &Ingress{},
input: &networking.HTTPRoute{},
expect: &networking.HTTPRoute{},
},
{
config: &Ingress{
Timeout: &TimeoutConfig{},
},
input: &networking.HTTPRoute{},
expect: &networking.HTTPRoute{},
},
{
config: &Ingress{
Timeout: &TimeoutConfig{
time: &types.Duration{},
},
},
input: &networking.HTTPRoute{},
expect: &networking.HTTPRoute{},
},
{
config: &Ingress{
Timeout: &TimeoutConfig{
time: &types.Duration{
Seconds: 10,
},
},
},
input: &networking.HTTPRoute{},
expect: &networking.HTTPRoute{
Timeout: &types.Duration{
Seconds: 10,
},
},
},
}
for _, inputCase := range inputCases {
t.Run("", func(t *testing.T) {
timeout.ApplyRoute(inputCase.input, inputCase.config)
if !reflect.DeepEqual(inputCase.input, inputCase.expect) {
t.Fatalf("Should be equal")
}
})
}
}

View File

@@ -140,17 +140,19 @@ func GetHost(annotations map[string]string) string {
return ""
}
// Istio requires that the name of the gateway must conform to the DNS label.
// For details, you can view: https://github.com/istio/istio/blob/2d5c40ad5e9cceebe64106005aa38381097da2ba/pkg/config/validation/validation.go#L478
func convertToDNSLabelValid(input string) string {
hasher := md5.New()
hasher.Write([]byte(input))
hash := hasher.Sum(nil)
return hex.EncodeToString(hash)
}
// CleanHost follow the format of mse-ops for host.
func CleanHost(host string) string {
if host == "*" {
return "global"
}
if strings.HasPrefix(host, "*") {
host = strings.ReplaceAll(host, "*", "global-")
}
return strings.ReplaceAll(host, ".", "-")
return convertToDNSLabelValid(host)
}
func CreateConvertedName(items ...string) string {

View File

@@ -35,11 +35,13 @@ type ItemEventHandler = func(name string)
type HigressConfig struct {
Tracing *Tracing `json:"tracing,omitempty"`
Gzip *Gzip `json:"gzip,omitempty"`
}
func NewDefaultHigressConfig() *HigressConfig {
higressConfig := &HigressConfig{
Tracing: NewDefaultTracing(),
Gzip: NewDefaultGzip(),
}
return higressConfig
}

View File

@@ -73,7 +73,9 @@ func NewConfigmapMgr(XDSUpdater model.XDSUpdater, namespace string, higressConfi
configmapMgr.SetHigressConfig(NewDefaultHigressConfig())
tracingController := NewTracingController(namespace)
gzipController := NewGzipController(namespace)
configmapMgr.AddItemControllers(tracingController)
configmapMgr.AddItemControllers(gzipController)
configmapMgr.initEventHandlers()
return configmapMgr

View File

@@ -0,0 +1,336 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// 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 configmap
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"
"sync/atomic"
"github.com/alibaba/higress/pkg/ingress/kube/util"
. "github.com/alibaba/higress/pkg/ingress/log"
networking "istio.io/api/networking/v1alpha3"
"istio.io/istio/pkg/config"
"istio.io/istio/pkg/config/schema/gvk"
)
const (
higressGzipEnvoyFilterName = "higress-config-gzip"
compressionStrategyValues = "DEFAULT_STRATEGY,FILTERED,HUFFMAN_ONLY,RLE,FIXED"
compressionLevelValues = "BEST_COMPRESSION,BEST_SPEED,COMPRESSION_LEVEL_1,COMPRESSION_LEVEL_2,COMPRESSION_LEVEL_3,COMPRESSION_LEVEL_4,COMPRESSION_LEVEL_5,COMPRESSION_LEVEL_6,COMPRESSION_LEVEL_7,COMPRESSION_LEVEL_8,COMPRESSION_LEVEL_9"
)
type Gzip struct {
// Flag to control gzip
Enable bool `json:"enable,omitempty"`
MinContentLength int32 `json:"minContentLength,omitempty"`
ContentType []string `json:"contentType,omitempty"`
DisableOnEtagHeader bool `json:"disableOnEtagHeader,omitempty"`
// Value from 1 to 9 that controls the amount of internal memory used by zlib.
// Higher values use more memory, but are faster and produce better compression results. The default value is 5.
MemoryLevel int32 `json:"memoryLevel,omitempty"`
// Value from 9 to 15 that represents the base two logarithmic of the compressors window size.
// Larger window results in better compression at the expense of memory usage.
// The default is 12 which will produce a 4096 bytes window
WindowBits int32 `json:"windowBits,omitempty"`
// Value for Zlibs next output buffer. If not set, defaults to 4096.
ChunkSize int32 `json:"chunkSize,omitempty"`
// A value used for selecting the zlib compression level.
// From COMPRESSION_LEVEL_1 to COMPRESSION_LEVEL_9
// BEST_COMPRESSION == COMPRESSION_LEVEL_9 , BEST_SPEED == COMPRESSION_LEVEL_1
CompressionLevel string `json:"compressionLevel,omitempty"`
// A value used for selecting the zlib compression strategy which is directly related to the characteristics of the content.
// Most of the time “DEFAULT_STRATEGY”
// Value is one of DEFAULT_STRATEGY, FILTERED, HUFFMAN_ONLY, RLE, FIXED
CompressionStrategy string `json:"compressionStrategy,omitempty"`
}
func validGzip(g *Gzip) error {
if g == nil {
return nil
}
if g.MinContentLength <= 0 {
return errors.New("minContentLength can not be less than zero")
}
if len(g.ContentType) == 0 {
return errors.New("content type can not be empty")
}
if !(g.MemoryLevel >= 1 && g.MemoryLevel <= 9) {
return errors.New("memory level need be between 1 and 9")
}
if !(g.WindowBits >= 9 && g.WindowBits <= 15) {
return errors.New("window bits need be between 9 and 15")
}
if g.ChunkSize <= 0 {
return errors.New("chunk size need be large than zero")
}
compressionLevels := strings.Split(compressionLevelValues, ",")
isFound := false
for _, v := range compressionLevels {
if g.CompressionLevel == v {
isFound = true
break
}
}
if !isFound {
return fmt.Errorf("compressionLevel need be one of %s", compressionLevelValues)
}
isFound = false
compressionStrategies := strings.Split(compressionStrategyValues, ",")
for _, v := range compressionStrategies {
if g.CompressionStrategy == v {
isFound = true
break
}
}
if !isFound {
return fmt.Errorf("compressionStrategy need be one of %s", compressionStrategyValues)
}
return nil
}
func compareGzip(old *Gzip, new *Gzip) (Result, error) {
if old == nil && new == nil {
return ResultNothing, nil
}
if new == nil {
return ResultDelete, nil
}
if !reflect.DeepEqual(old, new) {
return ResultReplace, nil
}
return ResultNothing, nil
}
func deepCopyGzip(gzip *Gzip) (*Gzip, error) {
newGzip := NewDefaultGzip()
bytes, err := json.Marshal(gzip)
if err != nil {
return nil, err
}
err = json.Unmarshal(bytes, newGzip)
return newGzip, err
}
func NewDefaultGzip() *Gzip {
gzip := &Gzip{
Enable: false,
MinContentLength: 1024,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
}
return gzip
}
type GzipController struct {
Namespace string
gzip atomic.Value
Name string
eventHandler ItemEventHandler
}
func NewGzipController(namespace string) *GzipController {
gzipController := &GzipController{
Namespace: namespace,
gzip: atomic.Value{},
Name: "gzip",
}
gzipController.SetGzip(NewDefaultGzip())
return gzipController
}
func (g *GzipController) GetName() string {
return g.Name
}
func (t *GzipController) SetGzip(gzip *Gzip) {
t.gzip.Store(gzip)
}
func (g *GzipController) GetGzip() *Gzip {
value := g.gzip.Load()
if value != nil {
if gzip, ok := value.(*Gzip); ok {
return gzip
}
}
return nil
}
func (g *GzipController) AddOrUpdateHigressConfig(name util.ClusterNamespacedName, old *HigressConfig, new *HigressConfig) error {
if err := validGzip(new.Gzip); err != nil {
IngressLog.Errorf("data:%+v convert to gzip , error: %+v", new.Gzip, err)
return nil
}
result, _ := compareGzip(old.Gzip, new.Gzip)
switch result {
case ResultReplace:
if newGzip, err := deepCopyGzip(new.Gzip); err != nil {
IngressLog.Infof("gzip deepcopy error:%v", err)
} else {
g.SetGzip(newGzip)
IngressLog.Infof("AddOrUpdate Higress config gzip")
g.eventHandler(higressGzipEnvoyFilterName)
IngressLog.Infof("send event with filter name:%s", higressGzipEnvoyFilterName)
}
case ResultDelete:
g.SetGzip(NewDefaultGzip())
IngressLog.Infof("Delete Higress config gzip")
g.eventHandler(higressGzipEnvoyFilterName)
IngressLog.Infof("send event with filter name:%s", higressGzipEnvoyFilterName)
}
return nil
}
func (g *GzipController) ValidHigressConfig(higressConfig *HigressConfig) error {
if higressConfig == nil {
return nil
}
if higressConfig.Gzip == nil {
return nil
}
return validGzip(higressConfig.Gzip)
}
func (g *GzipController) ConstructEnvoyFilters() ([]*config.Config, error) {
configs := make([]*config.Config, 0)
gzip := g.GetGzip()
namespace := g.Namespace
if gzip == nil {
return configs, nil
}
if gzip.Enable == false {
return configs, nil
}
gzipStruct := g.constructGzipStruct(gzip, namespace)
if len(gzipStruct) == 0 {
return configs, nil
}
config := &config.Config{
Meta: config.Meta{
GroupVersionKind: gvk.EnvoyFilter,
Name: higressGzipEnvoyFilterName,
Namespace: namespace,
},
Spec: &networking.EnvoyFilter{
ConfigPatches: []*networking.EnvoyFilter_EnvoyConfigObjectPatch{
{
ApplyTo: networking.EnvoyFilter_HTTP_FILTER,
Match: &networking.EnvoyFilter_EnvoyConfigObjectMatch{
Context: networking.EnvoyFilter_GATEWAY,
ObjectTypes: &networking.EnvoyFilter_EnvoyConfigObjectMatch_Listener{
Listener: &networking.EnvoyFilter_ListenerMatch{
FilterChain: &networking.EnvoyFilter_ListenerMatch_FilterChainMatch{
Filter: &networking.EnvoyFilter_ListenerMatch_FilterMatch{
Name: "envoy.filters.network.http_connection_manager",
SubFilter: &networking.EnvoyFilter_ListenerMatch_SubFilterMatch{
Name: "envoy.filters.http.router",
},
},
},
},
},
},
Patch: &networking.EnvoyFilter_Patch{
Operation: networking.EnvoyFilter_Patch_INSERT_BEFORE,
Value: util.BuildPatchStruct(gzipStruct),
},
},
},
},
}
configs = append(configs, config)
return configs, nil
}
func (g *GzipController) RegisterItemEventHandler(eventHandler ItemEventHandler) {
g.eventHandler = eventHandler
}
func (g *GzipController) constructGzipStruct(gzip *Gzip, namespace string) string {
gzipConfig := ""
contentType := ""
index := 0
for _, v := range gzip.ContentType {
contentType = contentType + fmt.Sprintf("\"%s\"", v)
if index < len(gzip.ContentType)-1 {
contentType = contentType + ","
}
index++
}
structFmt := `{
"name": "envoy.filters.http.compressor",
"typed_config": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.compressor.v3.Compressor",
"response_direction_config": {
"common_config": {
"min_content_length": %d,
"content_type": [%s],
"disable_on_etag_header": %t
}
},
"request_direction_config": {
"common_config": {
"enabled": {
"default_value": false,
"runtime_key": "request_compressor_enabled"
}
}
},
"compressor_library": {
"name": "text_optimized",
"typed_config": {
"@type": "type.googleapis.com/envoy.extensions.compression.gzip.compressor.v3.Gzip",
"memory_level": %d,
"window_bits": %d,
"check_size": %d,
"compression_level": "%s",
"compression_strategy": "%s"
}
}
}
}`
gzipConfig = fmt.Sprintf(structFmt, gzip.MinContentLength, contentType, gzip.DisableOnEtagHeader,
gzip.MemoryLevel, gzip.WindowBits, gzip.ChunkSize, gzip.CompressionLevel, gzip.CompressionStrategy)
return gzipConfig
}

View File

@@ -0,0 +1,495 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// 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 configmap
import (
"errors"
"fmt"
"github.com/alibaba/higress/pkg/ingress/kube/util"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_validGzip(t *testing.T) {
tests := []struct {
name string
gzip *Gzip
wantErr error
}{
{
name: "default",
gzip: &Gzip{
Enable: false,
MinContentLength: 1024,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
wantErr: nil,
},
{
name: "nil",
gzip: nil,
wantErr: nil,
},
{
name: "no content type",
gzip: &Gzip{
Enable: false,
MinContentLength: 1024,
ContentType: []string{},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
wantErr: errors.New("content type can not be empty"),
},
{
name: "MinContentLength less than zero",
gzip: &Gzip{
Enable: false,
MinContentLength: 0,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
wantErr: errors.New("minContentLength can not be less than zero"),
},
{
name: "MemoryLevel less than 1",
gzip: &Gzip{
Enable: false,
MinContentLength: 1024,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
wantErr: errors.New("memory level need be between 1 and 9"),
},
{
name: "WindowBits less than 9",
gzip: &Gzip{
Enable: false,
MinContentLength: 1024,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 8,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
wantErr: errors.New("window bits need be between 9 and 15"),
},
{
name: "ChunkSize less than zero",
gzip: &Gzip{
Enable: false,
MinContentLength: 1024,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
wantErr: errors.New("chunk size need be large than zero"),
},
{
name: "CompressionLevel is not right",
gzip: &Gzip{
Enable: false,
MinContentLength: 1024,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSIONA",
CompressionStrategy: "DEFAULT_STRATEGY",
},
wantErr: fmt.Errorf("compressionLevel need be one of %s", compressionLevelValues),
},
{
name: "CompressionStrategy is not right",
gzip: &Gzip{
Enable: false,
MinContentLength: 1024,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGYA",
},
wantErr: fmt.Errorf("compressionStrategy need be one of %s", compressionStrategyValues),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validGzip(tt.gzip); err != nil {
assert.Equal(t, tt.wantErr, err)
}
})
}
}
func Test_compareGzip(t *testing.T) {
tests := []struct {
name string
old *Gzip
new *Gzip
wantResult Result
wantErr error
}{
{
name: "compare both nil",
old: nil,
new: nil,
wantResult: ResultNothing,
wantErr: nil,
},
{
name: "compare result delete",
old: &Gzip{
Enable: false,
MinContentLength: 1024,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
new: nil,
wantResult: ResultDelete,
wantErr: nil,
},
{
name: "compare result equal",
old: &Gzip{
Enable: false,
MinContentLength: 1024,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
new: &Gzip{
Enable: false,
MinContentLength: 1024,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
wantResult: ResultNothing,
wantErr: nil,
},
{
name: "compare result replace",
old: &Gzip{
Enable: false,
MinContentLength: 1024,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
new: &Gzip{
Enable: true,
MinContentLength: 1024,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
wantResult: ResultReplace,
wantErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := compareGzip(tt.old, tt.new)
assert.Equal(t, tt.wantResult, result)
assert.Equal(t, tt.wantErr, err)
})
}
}
func Test_deepCopyGzip(t *testing.T) {
tests := []struct {
name string
gzip *Gzip
wantGzip *Gzip
wantErr error
}{
{
name: "deep copy",
gzip: &Gzip{
Enable: false,
MinContentLength: 1024,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
wantGzip: &Gzip{
Enable: false,
MinContentLength: 1024,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
wantErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gzip, err := deepCopyGzip(tt.gzip)
assert.Equal(t, tt.wantGzip, gzip)
assert.Equal(t, tt.wantErr, err)
})
}
}
func TestGzipController_AddOrUpdateHigressConfig(t *testing.T) {
eventPush := "default"
defaultHandler := func(name string) {
eventPush = "push"
}
defaultName := util.ClusterNamespacedName{}
tests := []struct {
name string
old *HigressConfig
new *HigressConfig
wantErr error
wantEventPush string
wantGzip *Gzip
}{
{
name: "default",
old: &HigressConfig{
Gzip: NewDefaultGzip(),
},
new: &HigressConfig{
Gzip: NewDefaultGzip(),
},
wantErr: nil,
wantEventPush: "default",
wantGzip: NewDefaultGzip(),
},
{
name: "replace and push 1",
old: &HigressConfig{
Gzip: NewDefaultGzip(),
},
new: &HigressConfig{
Gzip: &Gzip{
Enable: true,
MinContentLength: 1024,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
},
wantErr: nil,
wantEventPush: "push",
wantGzip: &Gzip{
Enable: true,
MinContentLength: 1024,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
},
{
name: "replace and push 2",
old: &HigressConfig{
Gzip: &Gzip{
Enable: true,
MinContentLength: 1024,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
},
new: &HigressConfig{
Gzip: &Gzip{
Enable: true,
MinContentLength: 2048,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
},
wantErr: nil,
wantEventPush: "push",
wantGzip: &Gzip{
Enable: true,
MinContentLength: 2048,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
},
{
name: "replace and push 3",
old: &HigressConfig{
Gzip: &Gzip{
Enable: true,
MinContentLength: 1024,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
},
new: &HigressConfig{
Gzip: &Gzip{
Enable: false,
MinContentLength: 2048,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
},
wantErr: nil,
wantEventPush: "push",
wantGzip: &Gzip{
Enable: false,
MinContentLength: 2048,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
},
{
name: "delete and push",
old: &HigressConfig{
Gzip: &Gzip{
Enable: true,
MinContentLength: 1024,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
},
new: &HigressConfig{
Gzip: nil,
},
wantErr: nil,
wantEventPush: "push",
wantGzip: NewDefaultGzip(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewGzipController("higress-system")
g.eventHandler = defaultHandler
eventPush = "default"
err := g.AddOrUpdateHigressConfig(defaultName, tt.old, tt.new)
assert.Equal(t, tt.wantEventPush, eventPush)
assert.Equal(t, tt.wantErr, err)
assert.Equal(t, tt.wantGzip, g.GetGzip())
})
}
}

View File

@@ -117,7 +117,7 @@ func validTracing(t *Tracing) error {
}
}
if tracerNum != 1 {
if tracerNum != 1 && t.Enable == true {
return errors.New("only one of skywalkingzipkin and opentelemetry configuration can be set")
}
return nil

View File

@@ -373,7 +373,6 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
}
for _, rule := range ingressV1Beta.Rules {
cleanHost := common.CleanHost(rule.Host)
// Need create builder for every rule.
domainBuilder := &common.IngressDomainBuilder{
ClusterId: c.options.ClusterId,
@@ -401,7 +400,7 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
Port: &networking.Port{
Number: 80,
Protocol: string(protocol.HTTP),
Name: common.CreateConvertedName("http-80-ingress", c.options.ClusterId, cfg.Namespace, cfg.Name, cleanHost),
Name: common.CreateConvertedName("http-80-ingress", c.options.ClusterId),
},
Hosts: []string{rule.Host},
})
@@ -446,7 +445,7 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
Port: &networking.Port{
Number: 443,
Protocol: string(protocol.HTTPS),
Name: common.CreateConvertedName("https-443-ingress", c.options.ClusterId, cfg.Namespace, cfg.Name, cleanHost),
Name: common.CreateConvertedName("https-443-ingress", c.options.ClusterId),
},
Hosts: []string{rule.Host},
Tls: &networking.ServerTLSSettings{
@@ -535,11 +534,11 @@ func (c *controller) ConvertHTTPRoute(convertOptions *common.ConvertOptions, wra
var pathType common.PathType
originPath := httpPath.Path
if annotationsConfig := wrapper.AnnotationsConfig; annotationsConfig.NeedRegexMatch() {
if annotationsConfig.IsPrefixRegexMatch() {
pathType = common.PrefixRegex
} else if annotationsConfig.IsFullPathRegexMatch() {
if annotationsConfig := wrapper.AnnotationsConfig; annotationsConfig.NeedRegexMatch(originPath) {
if annotationsConfig.IsFullPathRegexMatch() {
pathType = common.FullPathRegex
} else {
pathType = common.PrefixRegex
}
} else {
switch *httpPath.PathType {
@@ -712,7 +711,7 @@ func (c *controller) ApplyCanaryIngress(convertOptions *common.ConvertOptions, w
return fmt.Errorf("wrapperConfig is nil")
}
byHeader, byWeight := wrapper.AnnotationsConfig.CanaryKind()
byHeader, _ := wrapper.AnnotationsConfig.CanaryKind()
cfg := wrapper.Config
ingressV1Beta, ok := cfg.Spec.(ingress.IngressSpec)
@@ -746,11 +745,11 @@ func (c *controller) ApplyCanaryIngress(convertOptions *common.ConvertOptions, w
var pathType common.PathType
originPath := httpPath.Path
if annotationsConfig := wrapper.AnnotationsConfig; annotationsConfig.NeedRegexMatch() {
if annotationsConfig.IsPrefixRegexMatch() {
pathType = common.PrefixRegex
} else if annotationsConfig.IsFullPathRegexMatch() {
if annotationsConfig := wrapper.AnnotationsConfig; annotationsConfig.NeedRegexMatch(originPath) {
if annotationsConfig.IsFullPathRegexMatch() {
pathType = common.FullPathRegex
} else {
pathType = common.PrefixRegex
}
} else {
switch *httpPath.PathType {
@@ -765,9 +764,6 @@ func (c *controller) ApplyCanaryIngress(convertOptions *common.ConvertOptions, w
}
canary.OriginPath = originPath
canary.OriginPathType = pathType
canary.HTTPRoute.Match = c.generateHttpMatches(pathType, httpPath.Path, nil)
canary.HTTPRoute.Name = common.GenerateUniqueRouteName(c.options.SystemNamespace, canary)
ingressRouteBuilder := convertOptions.IngressRouteCache.New(canary)
// backend service check
var event common.Event
@@ -781,39 +777,37 @@ func (c *controller) ApplyCanaryIngress(convertOptions *common.ConvertOptions, w
}
canary.RuleKey = createRuleKey(canary.WrapperConfig.Config.Annotations, canary.PathFormat())
canaryConfig := wrapper.AnnotationsConfig.Canary
if byWeight {
canary.HTTPRoute.Route[0].Weight = int32(canaryConfig.Weight)
}
// find the base ingress
pos := 0
var targetRoute *common.WrapperHTTPRoute
for _, route := range routes {
if isCanaryRoute(canary, route) {
targetRoute = route
// Header, Cookie
if byHeader {
IngressLog.Debug("Insert canary route by header")
annotations.ApplyByHeader(canary.HTTPRoute, route.HTTPRoute, canary.WrapperConfig.AnnotationsConfig)
canary.HTTPRoute.Name = common.GenerateUniqueRouteName(c.options.SystemNamespace, canary)
} else {
IngressLog.Debug("Merge canary route by weight")
if route.WeightTotal == 0 {
route.WeightTotal = int32(canaryConfig.WeightTotal)
}
annotations.ApplyByWeight(canary.HTTPRoute, route.HTTPRoute, canary.WrapperConfig.AnnotationsConfig)
}
break
}
pos += 1
}
IngressLog.Debugf("Canary route is %v", canary)
if targetRoute == nil {
continue
}
canaryConfig := wrapper.AnnotationsConfig.Canary
// Header, Cookie
if byHeader {
IngressLog.Debug("Insert canary route by header")
annotations.ApplyByHeader(canary.HTTPRoute, targetRoute.HTTPRoute, canary.WrapperConfig.AnnotationsConfig)
canary.HTTPRoute.Name = common.GenerateUniqueRouteName(c.options.SystemNamespace, canary)
} else {
IngressLog.Debug("Merge canary route by weight")
if targetRoute.WeightTotal == 0 {
targetRoute.WeightTotal = int32(canaryConfig.WeightTotal)
}
annotations.ApplyByWeight(canary.HTTPRoute, targetRoute.HTTPRoute, canary.WrapperConfig.AnnotationsConfig)
}
IngressLog.Debugf("Canary route is %v", canary)
if byHeader {
// Inherit policy from normal route
canary.WrapperConfig.AnnotationsConfig.Auth = targetRoute.WrapperConfig.AnnotationsConfig.Auth

View File

@@ -358,7 +358,6 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
}
for _, rule := range ingressV1.Rules {
cleanHost := common.CleanHost(rule.Host)
// Need create builder for every rule.
domainBuilder := &common.IngressDomainBuilder{
ClusterId: c.options.ClusterId,
@@ -386,7 +385,7 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
Port: &networking.Port{
Number: 80,
Protocol: string(protocol.HTTP),
Name: common.CreateConvertedName("http-80-ingress", c.options.ClusterId, cfg.Namespace, cfg.Name, cleanHost),
Name: common.CreateConvertedName("http-80-ingress", c.options.ClusterId),
},
Hosts: []string{rule.Host},
})
@@ -431,7 +430,7 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
Port: &networking.Port{
Number: 443,
Protocol: string(protocol.HTTPS),
Name: common.CreateConvertedName("https-443-ingress", c.options.ClusterId, cfg.Namespace, cfg.Name, cleanHost),
Name: common.CreateConvertedName("https-443-ingress", c.options.ClusterId),
},
Hosts: []string{rule.Host},
Tls: &networking.ServerTLSSettings{
@@ -517,11 +516,11 @@ func (c *controller) ConvertHTTPRoute(convertOptions *common.ConvertOptions, wra
var pathType common.PathType
originPath := httpPath.Path
if annotationsConfig := wrapper.AnnotationsConfig; annotationsConfig.NeedRegexMatch() {
if annotationsConfig.IsPrefixRegexMatch() {
pathType = common.PrefixRegex
} else if annotationsConfig.IsFullPathRegexMatch() {
if annotationsConfig := wrapper.AnnotationsConfig; annotationsConfig.NeedRegexMatch(originPath) {
if annotationsConfig.IsFullPathRegexMatch() {
pathType = common.FullPathRegex
} else {
pathType = common.PrefixRegex
}
} else {
switch *httpPath.PathType {
@@ -716,7 +715,7 @@ func (c *controller) ApplyDefaultBackend(convertOptions *common.ConvertOptions,
}
func (c *controller) ApplyCanaryIngress(convertOptions *common.ConvertOptions, wrapper *common.WrapperConfig) error {
byHeader, byWeight := wrapper.AnnotationsConfig.CanaryKind()
byHeader, _ := wrapper.AnnotationsConfig.CanaryKind()
cfg := wrapper.Config
ingressV1, ok := cfg.Spec.(ingress.IngressSpec)
@@ -750,11 +749,11 @@ func (c *controller) ApplyCanaryIngress(convertOptions *common.ConvertOptions, w
var pathType common.PathType
originPath := httpPath.Path
if annotationsConfig := wrapper.AnnotationsConfig; annotationsConfig.NeedRegexMatch() {
if annotationsConfig.IsPrefixRegexMatch() {
pathType = common.PrefixRegex
} else if annotationsConfig.IsFullPathRegexMatch() {
if annotationsConfig := wrapper.AnnotationsConfig; annotationsConfig.NeedRegexMatch(originPath) {
if annotationsConfig.IsFullPathRegexMatch() {
pathType = common.FullPathRegex
} else {
pathType = common.PrefixRegex
}
} else {
switch *httpPath.PathType {
@@ -769,8 +768,6 @@ func (c *controller) ApplyCanaryIngress(convertOptions *common.ConvertOptions, w
}
canary.OriginPath = originPath
canary.OriginPathType = pathType
canary.HTTPRoute.Match = c.generateHttpMatches(pathType, httpPath.Path, nil)
canary.HTTPRoute.Name = common.GenerateUniqueRouteName(c.options.SystemNamespace, canary)
ingressRouteBuilder := convertOptions.IngressRouteCache.New(canary)
// backend service check
@@ -785,39 +782,37 @@ func (c *controller) ApplyCanaryIngress(convertOptions *common.ConvertOptions, w
}
canary.RuleKey = createRuleKey(canary.WrapperConfig.Config.Annotations, canary.PathFormat())
canaryConfig := wrapper.AnnotationsConfig.Canary
if byWeight {
canary.HTTPRoute.Route[0].Weight = int32(canaryConfig.Weight)
}
// find the base ingress
pos := 0
var targetRoute *common.WrapperHTTPRoute
for _, route := range routes {
if isCanaryRoute(canary, route) {
targetRoute = route
// Header, Cookie
if byHeader {
IngressLog.Debug("Insert canary route by header")
annotations.ApplyByHeader(canary.HTTPRoute, route.HTTPRoute, canary.WrapperConfig.AnnotationsConfig)
canary.HTTPRoute.Name = common.GenerateUniqueRouteName(c.options.SystemNamespace, canary)
} else {
IngressLog.Debug("Merge canary route by weight")
if route.WeightTotal == 0 {
route.WeightTotal = int32(canaryConfig.WeightTotal)
}
annotations.ApplyByWeight(canary.HTTPRoute, route.HTTPRoute, canary.WrapperConfig.AnnotationsConfig)
}
break
}
pos += 1
}
IngressLog.Debugf("Canary route is %v", canary)
if targetRoute == nil {
continue
}
canaryConfig := wrapper.AnnotationsConfig.Canary
// Header, Cookie
if byHeader {
IngressLog.Debug("Insert canary route by header")
annotations.ApplyByHeader(canary.HTTPRoute, targetRoute.HTTPRoute, canary.WrapperConfig.AnnotationsConfig)
canary.HTTPRoute.Name = common.GenerateUniqueRouteName(c.options.SystemNamespace, canary)
} else {
IngressLog.Debug("Merge canary route by weight")
if targetRoute.WeightTotal == 0 {
targetRoute.WeightTotal = int32(canaryConfig.WeightTotal)
}
annotations.ApplyByWeight(canary.HTTPRoute, targetRoute.HTTPRoute, canary.WrapperConfig.AnnotationsConfig)
}
IngressLog.Debugf("Canary route is %v", canary)
if byHeader {
// Inherit policy from normal route
canary.WrapperConfig.AnnotationsConfig.Auth = targetRoute.WrapperConfig.AnnotationsConfig.Auth

View File

@@ -16,7 +16,6 @@ package kingress
import (
"fmt"
"github.com/alibaba/higress/pkg/ingress/kube/annotations"
"path"
"reflect"
"sort"
@@ -24,7 +23,6 @@ import (
"sync"
"time"
"github.com/alibaba/higress/pkg/kube"
"github.com/hashicorp/go-multierror"
networking "istio.io/api/networking/v1alpha3"
"istio.io/istio/pilot/pkg/model"
@@ -46,10 +44,12 @@ import (
ingress "knative.dev/networking/pkg/apis/networking/v1alpha1"
networkingv1alpha1 "knative.dev/networking/pkg/client/listers/networking/v1alpha1"
"github.com/alibaba/higress/pkg/ingress/kube/annotations"
"github.com/alibaba/higress/pkg/ingress/kube/common"
"github.com/alibaba/higress/pkg/ingress/kube/kingress/resources"
"github.com/alibaba/higress/pkg/ingress/kube/secret"
. "github.com/alibaba/higress/pkg/ingress/log"
"github.com/alibaba/higress/pkg/kube"
)
var (
@@ -337,7 +337,6 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
for _, rule := range kingressv1alpha1.Rules {
for _, ruleHost := range rule.Hosts {
cleanHost := common.CleanHost(ruleHost)
// Need create builder for every rule.
domainBuilder := &common.IngressDomainBuilder{
ClusterId: c.options.ClusterId,
@@ -364,7 +363,7 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
Port: &networking.Port{
Number: 8081,
Protocol: string(protocol.HTTP),
Name: common.CreateConvertedName("http-8081-ingress", c.options.ClusterId, cfg.Namespace, cfg.Name, cleanHost),
Name: common.CreateConvertedName("http-8081-ingress", c.options.ClusterId),
},
Hosts: []string{ruleHost},
})
@@ -374,7 +373,7 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
Port: &networking.Port{
Number: 80,
Protocol: string(protocol.HTTP),
Name: common.CreateConvertedName("http-80-ingress", c.options.ClusterId, cfg.Namespace, cfg.Name, cleanHost),
Name: common.CreateConvertedName("http-80-ingress", c.options.ClusterId),
},
Hosts: []string{ruleHost},
})
@@ -436,7 +435,7 @@ func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapp
Port: &networking.Port{
Number: 443,
Protocol: string(protocol.HTTPS),
Name: common.CreateConvertedName("https-443-ingress", c.options.ClusterId, cfg.Namespace, cfg.Name, cleanHost),
Name: common.CreateConvertedName("https-443-ingress", c.options.ClusterId),
},
Hosts: []string{ruleHost},
Tls: &networking.ServerTLSSettings{

View File

@@ -387,6 +387,8 @@ class RouteRuleMatcher {
return true;
}
request_host = Wasm::Common::Http::stripPortFromHost(request_host);
for (const auto& host_match : rule.hosts) {
const auto& host = host_match.second;
switch (host_match.first) {

View File

@@ -584,6 +584,48 @@ TEST_F(BasicAuthTest, RuleWithConsumerAllow) {
FilterHeadersStatus::Continue);
}
TEST_F(BasicAuthTest, GlobalAuthRuleWithDomainPort) {
std::string configuration = R"(
{
"global_auth": true,
"consumers" : [
{"credential" : "ok:test", "name" : "consumer_ok"},
{"credential" : "admin2:admin2", "name" : "consumer2"},
{"credential" : "YWRtaW4zOmFkbWluMw==", "name" : "consumer3"},
{"credential" : "admin:admin", "name" : "consumer"}
],
"_rules_" : [
{
"_match_domain_" : ["test.com", "*.example.com"],
"allow" : [ "consumer" ]
}
]
})";
BufferBase buffer;
buffer.set({configuration.data(), configuration.size()});
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
EXPECT_TRUE(root_context_->configure(configuration.size()));
authority_ = "www.example.com:8080";
cred_ = "admin:admin";
authorization_header_ = "Basic " + Base64::encode(cred_.data(), cred_.size());
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::Continue);
cred_ = "admin2:admin2";
authorization_header_ = "Basic " + Base64::encode(cred_.data(), cred_.size());
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopIteration);
authority_ = "abc.com";
authorization_header_ = "Basic " + Base64::encode(cred_.data(), cred_.size());
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::Continue);
}
TEST_F(BasicAuthTest, RuleWithEncryptedConsumerAllow) {
std::string configuration = R"(
{

View File

@@ -347,13 +347,13 @@ bool PluginRootContext::checkPlugin(
deniedInvalidDate();
return false;
}
time_offset = std::abs((long long)(timestamp - current_time));
// milliseconds to nanoseconds
time_offset *= 1e6;
timestamp *= 1e6;
// seconds
if (date.size() < MILLISEC_MIN_LENGTH) {
time_offset *= 1e3;
timestamp *= 1e3;
}
time_offset = std::abs((long long)(timestamp - current_time));
}
if (time_offset > rule.date_nano_offset) {
LOG_DEBUG(absl::StrFormat("date expired, offset is: %u",

View File

@@ -0,0 +1,80 @@
# Copyright (c) 2022 Alibaba Group Holding Ltd.
#
# 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.
load("@proxy_wasm_cpp_sdk//bazel:defs.bzl", "proxy_wasm_cc_binary")
load("//bazel:wasm.bzl", "declare_wasm_image_targets")
proxy_wasm_cc_binary(
name = "oauth.wasm",
srcs = [
"plugin.cc",
"plugin.h",
],
deps = [
"//common:random_util",
"@com_github_thalhammer_jwt_cpp//:lib",
"@com_github_mariusbancila_stduuid//:lib",
"@com_google_absl//absl/container:btree",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/strings:str_format",
"@com_google_absl//absl/time",
"@boringssl//:ssl",
"//common:json_util",
"//common:http_util",
"//common:rule_util",
],
)
cc_library(
name = "oauth_lib",
srcs = [
"plugin.cc",
],
hdrs = [
"plugin.h",
],
copts = ["-DNULL_PLUGIN"],
deps = [
"@com_github_thalhammer_jwt_cpp//:lib",
"@com_github_mariusbancila_stduuid//:lib",
"@com_google_absl//absl/container:btree",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/strings:str_format",
"@com_google_absl//absl/time",
"@boringssl//:ssl",
"//common:json_util",
"@proxy_wasm_cpp_host//:lib",
"//common:http_util_nullvm",
"//common:rule_util_nullvm",
],
)
cc_test(
name = "oauth_test",
srcs = [
"plugin_test.cc",
],
copts = ["-DNULL_PLUGIN"],
deps = [
":oauth_lib",
"@com_google_googletest//:gtest",
"@com_google_googletest//:gtest_main",
"@proxy_wasm_cpp_host//:lib",
],
)
declare_wasm_image_targets(
name = "oauth",
wasm_file = ":oauth.wasm",
)

View File

@@ -0,0 +1,129 @@
# 功能说明
`OAuth2`插件实现了基于JWT(JSON Web Tokens)进行OAuth2 Access Token签发的能力, 遵循[RFC9068](https://datatracker.ietf.org/doc/html/rfc9068)规范
# 插件配置说明
## 配置字段
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
| ----------- | --------------- | ------------------------------------------- | ------ | ----------------------------------------------------------- |
| `consumers` | array of object | 必填 | - | 配置服务的调用者,用于对请求进行认证 |
| `_rules_` | array of object | 选填 | - | 配置特定路由或域名的访问权限列表,用于对请求进行鉴权 |
| `issuer` | string | 选填 | Higress-Gateway | 用于填充JWT中的issuer |
| `auth_path` | string | 选填 | /oauth2/token | 指定路径后缀用于签发Token路由级配置时要确保首先能匹配对应的路由 |
| `global_credentials` | bool | 选填 | ture | 是否开启全局凭证即允许路由A下的auth_path签发的Token可以用于访问路由B |
| `auth_header_name` | string | 选填 | Authorization | 用于指定从哪个请求头获取JWT |
| `token_ttl` | number | 选填 | 7200 | token从签发后多久内有效单位为秒 |
| `clock_skew_seconds` | number | 选填 | 60 | 校验JWT的exp和iat字段时允许的时钟偏移量单位为秒 |
| `keep_token` | bool | 选填 | ture | 转发给后端时是否保留JWT |
`consumers`中每一项的配置字段说明如下:
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
| ----------------------- | ----------------- | -------- | ------------------------------------------------- | ------------------------ |
| `name` | string | 必填 | - | 配置该consumer的名称 |
| `client_id` | string | 必填 | - | OAuth2 client id |
| `client_secret` | string | 必填 | - | OAuth2 client secret |
`_rules_` 中每一项的配置字段说明如下:
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
| ---------------- | --------------- | ------------------------------------------------- | ------ | -------------------------------------------------- |
| `_match_route_` | array of string | 选填,`_match_route_``_match_domain_`中选填一项 | - | 配置要匹配的路由名称 |
| `_match_domain_` | array of string | 选填,`_match_route_``_match_domain_`中选填一项 | - | 配置要匹配的域名 |
| `allow` | array of string | 必填 | - | 对于符合匹配条件的请求配置允许访问的consumer名称 |
**注意:**
- 对于开启该配置的路由,如果路径后缀和`auth_path`匹配则该路由到原目标服务而是用于生成Token
- 如果关闭`global_credentials`,请确保启用此插件的路由不是精确匹配路由,此时若存在另一条前缀匹配路由,则可能导致预期外行为
- 若不配置`_rules_`字段,则默认对当前网关实例的所有路由开启认证;
- 对于通过认证鉴权的请求请求的header会被添加一个`X-Mse-Consumer`字段,用以标识调用者的名称。
## 配置示例
### 对特定路由或域名开启
以下配置将对网关特定路由或域名开启 Jwt Auth 认证和鉴权注意如果一个JWT能匹配多个`jwks`,则按照配置顺序命中第一个匹配的`consumer`
```yaml
consumers:
- name: consumer1
client_id: 12345678-xxxx-xxxx-xxxx-xxxxxxxxxxxx
client_secret: abcdefgh-xxxx-xxxx-xxxx-xxxxxxxxxxxx
- name: consumer2
client_id: 87654321-xxxx-xxxx-xxxx-xxxxxxxxxxxx
client_secret: hgfedcba-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# 使用 _rules_ 字段进行细粒度规则配置
_rules_:
# 规则一:按路由名称匹配生效
- _match_route_:
- route-a
- route-b
allow:
- consumer1
# 规则二:按域名匹配生效
- _match_domain_:
- "*.example.com"
- test.com
allow:
- consumer2
```
此例 `_match_route_` 中指定的 `route-a``route-b` 即在创建网关路由时填写的路由名称,当匹配到这两个路由时,将允许`name``consumer1`的调用者访问,其他调用者不允许访问;
此例 `_match_domain_` 中指定的 `*.example.com``test.com` 用于匹配请求的域名,当发现域名匹配时,将允许`name``consumer2`的调用者访问,其他调用者不允许访问。
#### 使用 Client Credential 授权模式
**获取 AccessToken**
```bash
# 通过 GET 方法获取
curl 'http://test.com/oauth2/token?grant_type=client_credentials&client_id=12345678-xxxx-xxxx-xxxx-xxxxxxxxxxxx&client_secret=abcdefgh-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
# 通过 POST 方法获取 (需要先匹配到有真实目标服务的路由)
curl 'http://test.com/oauth2/token' -H 'content-type: application/x-www-form-urlencoded' -d 'grant_type=client_credentials&client_id=12345678-xxxx-xxxx-xxxx-xxxxxxxxxxxx&client_secret=abcdefgh-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
# 获取响应中的 access_token 字段即可:
{
"token_type": "bearer",
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6ImFwcGxpY2F0aW9uXC9hdCtqd3QifQ.eyJhdWQiOiJkZWZhdWx0IiwiY2xpZW50X2lkIjoiMTIzNDU2NzgteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4IiwiZXhwIjoxNjg3OTUxNDYzLCJpYXQiOjE2ODc5NDQyNjMsImlzcyI6IkhpZ3Jlc3MtR2F0ZXdheSIsImp0aSI6IjEwOTU5ZDFiLThkNjEtNGRlYy1iZWE3LTk0ODEwMzc1YjYzYyIsInN1YiI6ImNvbnN1bWVyMSJ9.NkT_rG3DcV9543vBQgneVqoGfIhVeOuUBwLJJ4Wycb0",
"expires_in": 7200
}
```
**使用 AccessToken 请求**
```bash
curl 'http://test.com' -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6ImFwcGxpY2F0aW9uXC9hdCtqd3QifQ.eyJhdWQiOiJkZWZhdWx0IiwiY2xpZW50X2lkIjoiMTIzNDU2NzgteHh4eC14eHh4LXh4eHgteHh4eHh4eHh4eHh4IiwiZXhwIjoxNjg3OTUxNDYzLCJpYXQiOjE2ODc5NDQyNjMsImlzcyI6IkhpZ3Jlc3MtR2F0ZXdheSIsImp0aSI6IjEwOTU5ZDFiLThkNjEtNGRlYy1iZWE3LTk0ODEwMzc1YjYzYyIsInN1YiI6ImNvbnN1bWVyMSJ9.NkT_rG3DcV9543vBQgneVqoGfIhVeOuUBwLJJ4Wycb0'
```
因为 test.com 仅授权了 consumer2但这个 Access Token 是基于 consumer1 的 `client_id``client_secret` 获取的,因此将返回 `403 Access Denied`
### 网关实例级别开启
以下配置未指定`_rules_`字段,因此将对网关实例级别开启 OAuth2 认证
```yaml
consumers:
- name: consumer1
client_id: 12345678-xxxx-xxxx-xxxx-xxxxxxxxxxxx
client_secret: abcdefgh-xxxx-xxxx-xxxx-xxxxxxxxxxxx
- name: consumer2
client_id: 87654321-xxxx-xxxx-xxxx-xxxxxxxxxxxx
client_secret: hgfedcba-xxxx-xxxx-xxxx-xxxxxxxxxxxx
```
# 常见错误码说明
| HTTP 状态码 | 出错信息 | 原因说明 |
| ----------- | ---------------------- | -------------------------------------------------------------------------------- |
| 401 | Invalid Jwt token | 请求头未提供JWT, 或者JWT格式错误或过期等原因 |
| 403 | Access Denied | 无权限访问当前路由 |

View File

@@ -0,0 +1,463 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// 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.
#include "extensions/oauth/plugin.h"
#include <algorithm>
#include <array>
#include <chrono>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <system_error>
#include <unordered_set>
#include <utility>
#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_join.h"
#include "absl/strings/str_split.h"
#include "common/common_util.h"
#include "common/http_util.h"
#include "common/json_util.h"
#include "uuid.h"
using ::nlohmann::json;
using ::Wasm::Common::JsonArrayIterate;
using ::Wasm::Common::JsonGetField;
using ::Wasm::Common::JsonObjectIterate;
using ::Wasm::Common::JsonValueAs;
#ifdef NULL_PLUGIN
namespace proxy_wasm {
namespace null_plugin {
namespace oauth {
PROXY_WASM_NULL_PLUGIN_REGISTRY
#endif
namespace {
constexpr absl::string_view TokenResponseTemplate = R"(
{
"token_type": "bearer",
"access_token": "%s",
"expires_in": %u
})";
const std::string& DefaultAudience = "default";
const std::string& TypeHeader = "application/at+jwt";
const std::string& BearerPrefix = "Bearer ";
const std::string& ClientCredentialsGrant = "client_credentials";
constexpr uint32_t MaximumUriLength = 256;
constexpr std::string_view kRcDetailOAuthPrefix = "oauth_access_denied";
std::string generateRcDetails(std::string_view error_msg) {
// Replace space with underscore since RCDetails may be written to access log.
// Some log processors assume each log segment is separated by whitespace.
return absl::StrCat(kRcDetailOAuthPrefix, "{",
absl::StrJoin(absl::StrSplit(error_msg, ' '), "_"), "}");
}
} // namespace
static RegisterContextFactory register_OAuth(CONTEXT_FACTORY(PluginContext),
ROOT_FACTORY(PluginRootContext));
#define JSON_FIND_FIELD(dict, field) \
auto dict##_##field##_json = dict.find(#field); \
if (dict##_##field##_json == dict.end()) { \
LOG_WARN("can't find '" #field "' in " #dict); \
return false; \
}
#define JSON_VALUE_AS(type, src, dst, err_msg) \
auto dst##_v = JsonValueAs<type>(src); \
if (dst##_v.second != Wasm::Common::JsonParserResultDetail::OK || \
!dst##_v.first) { \
LOG_WARN(#err_msg); \
return false; \
} \
auto& dst = dst##_v.first.value();
#define JSON_FIELD_VALUE_AS(type, dict, field) \
JSON_VALUE_AS(type, dict##_##field##_json.value(), dict##_##field, \
"'" #field "' field in " #dict "convert to " #type " failed")
bool PluginRootContext::generateToken(const OAuthConfigRule& rule,
const std::string& route_name,
const absl::string_view& raw_params,
std::string* token,
std::string* err_msg) {
auto params = Wasm::Common::Http::parseParameters(raw_params, 0, true);
auto it = params.find("grant_type");
if (it == params.end()) {
*err_msg = "grant_type is missing";
return false;
}
if (it->second != ClientCredentialsGrant) {
*err_msg = absl::StrFormat("grant_type:%s is not support", it->second);
return false;
}
it = params.find("client_id");
if (it == params.end()) {
*err_msg = "client_id is missing";
return false;
}
auto c_it = rule.consumers.find(it->second);
if (c_it == rule.consumers.end()) {
*err_msg = "invalid client_id or client_secret";
return false;
}
const auto& consumer = c_it->second;
it = params.find("client_secret");
if (it == params.end()) {
*err_msg = "client_secret is missing";
return false;
}
if (it->second != consumer.client_secret) {
*err_msg = "invalid client_id or client_secret";
return false;
}
auto jwt = jwt::create();
if (rule.global_credentials) {
jwt.set_audience(DefaultAudience);
} else {
jwt.set_audience(route_name);
}
it = params.find("scope");
if (it != params.end()) {
jwt.set_payload_claim("scope", jwt::claim(it->second));
}
std::random_device rd;
auto seed_data = std::array<int, std::mt19937::state_size>{};
std::generate(std::begin(seed_data), std::end(seed_data), std::ref(rd));
std::seed_seq seq(std::begin(seed_data), std::end(seed_data));
std::mt19937 generator(seq);
uuids::uuid_random_generator gen{generator};
std::error_code ec;
*token = jwt.set_issuer(rule.issuer)
.set_type(TypeHeader)
.set_subject(consumer.name)
.set_issued_at(std::chrono::system_clock::now())
.set_expires_at(std::chrono::system_clock::now() +
std::chrono::seconds{rule.token_ttl})
.set_payload_claim("client_id", jwt::claim(consumer.client_id))
.set_id(uuids::to_string(gen()))
.sign(jwt::algorithm::hs256{consumer.client_secret}, ec);
if (ec) {
*err_msg = absl::StrCat("jwt sign failed: %s", ec.message());
return false;
}
return true;
}
bool PluginRootContext::parsePluginConfig(const json& conf,
OAuthConfigRule& rule) {
std::unordered_set<std::string> name_set;
if (!JsonArrayIterate(conf, "consumers", [&](const json& consumer) -> bool {
Consumer c;
JSON_FIND_FIELD(consumer, name);
JSON_FIELD_VALUE_AS(std::string, consumer, name);
if (name_set.count(consumer_name) != 0) {
LOG_WARN("consumer already exists: " + consumer_name);
return false;
}
c.name = consumer_name;
JSON_FIND_FIELD(consumer, client_id);
JSON_FIELD_VALUE_AS(std::string, consumer, client_id);
c.client_id = consumer_client_id;
if (rule.consumers.find(c.client_id) != rule.consumers.end()) {
LOG_WARN("consumer client_id already exists: " + c.client_id);
return false;
}
JSON_FIND_FIELD(consumer, client_secret);
JSON_FIELD_VALUE_AS(std::string, consumer, client_secret);
c.client_secret = consumer_client_secret;
rule.consumers.emplace(c.client_id, std::move(c));
name_set.insert(consumer_name);
return true;
})) {
LOG_WARN("failed to parse configuration for consumers.");
return false;
}
// if (rule.consumers.empty()) {
// LOG_INFO("at least one consumer has to be configured for a rule.");
// return false;
// }
auto conf_issuer_json = conf.find("issuer");
if (conf_issuer_json != conf.end()) {
JSON_FIELD_VALUE_AS(std::string, conf, issuer);
rule.issuer = conf_issuer;
}
auto conf_auth_header_json = conf.find("auth_header");
if (conf_auth_header_json != conf.end()) {
JSON_FIELD_VALUE_AS(std::string, conf, auth_header);
rule.auth_header_name = conf_auth_header;
}
auto conf_auth_path_json = conf.find("auth_path");
if (conf_auth_path_json != conf.end()) {
JSON_FIELD_VALUE_AS(std::string, conf, auth_path);
if (conf_auth_path.empty()) {
conf_auth_path = "/";
} else if (conf_auth_path[0] != '/') {
conf_auth_path = absl::StrCat("/", conf_auth_path);
}
rule.auth_path = conf_auth_path;
}
auto conf_global_credentials_json = conf.find("global_credentials");
if (conf_global_credentials_json != conf.end()) {
JSON_FIELD_VALUE_AS(bool, conf, global_credentials);
rule.global_credentials = conf_global_credentials;
}
auto conf_token_ttl_json = conf.find("token_ttl");
if (conf_token_ttl_json != conf.end()) {
JSON_FIELD_VALUE_AS(uint64_t, conf, token_ttl);
rule.token_ttl = conf_token_ttl;
}
auto conf_keep_token_json = conf.find("keep_token");
if (conf_keep_token_json != conf.end()) {
JSON_FIELD_VALUE_AS(bool, conf, keep_token);
rule.keep_token = conf_keep_token;
}
auto conf_clock_skew_seconds_json = conf.find("clock_skew_seconds");
if (conf_clock_skew_seconds_json != conf.end()) {
JSON_FIELD_VALUE_AS(uint64_t, conf, clock_skew_seconds);
rule.clock_skew = conf_clock_skew_seconds;
}
return true;
}
#define CLAIM_CHECK(token, claim, type) \
if (!token.has_payload_claim(#claim)) { \
LOG_DEBUG("claim is missing: " #claim); \
goto failed; \
} \
if (token.get_payload_claim(#claim).get_type() != type) { \
LOG_DEBUG("claim is invalid: " #claim); \
goto failed; \
}
bool PluginRootContext::checkPlugin(
const OAuthConfigRule& rule,
const std::optional<std::unordered_set<std::string>>& allow_set,
const std::string& route_name) {
auto auth_header = getRequestHeader(rule.auth_header_name)->toString();
bool verified = false;
std::string token_str;
{
size_t pos;
if (auth_header.empty()) {
LOG_DEBUG("auth header is empty");
goto failed;
}
pos = auth_header.find(BearerPrefix);
if (pos == std::string::npos) {
LOG_DEBUG("auth header is not a bearer token");
goto failed;
}
auto start = pos + BearerPrefix.size();
token_str =
std::string{auth_header.c_str() + start, auth_header.size() - start};
auto token = jwt::decode(token_str);
CLAIM_CHECK(token, client_id, jwt::json::type::string);
CLAIM_CHECK(token, iss, jwt::json::type::string);
CLAIM_CHECK(token, sub, jwt::json::type::string);
CLAIM_CHECK(token, aud, jwt::json::type::string);
CLAIM_CHECK(token, exp, jwt::json::type::integer);
CLAIM_CHECK(token, iat, jwt::json::type::integer);
auto client_id = token.get_payload_claim("client_id").as_string();
auto it = rule.consumers.find(client_id);
if (it == rule.consumers.end()) {
LOG_DEBUG(absl::StrFormat("client_id not found:%s", client_id));
goto failed;
}
auto consumer = it->second;
auto verifier =
jwt::verify()
.allow_algorithm(jwt::algorithm::hs256{consumer.client_secret})
.with_issuer(rule.issuer)
.with_subject(consumer.name)
.with_type(TypeHeader)
.leeway(rule.clock_skew);
std::error_code ec;
verifier.verify(token, ec);
if (ec) {
LOG_INFO(absl::StrFormat("token verify failed, token:%s, reason:%s",
token_str, ec.message()));
goto failed;
}
verified = true;
if (allow_set &&
allow_set.value().find(consumer.name) == allow_set.value().end()) {
LOG_DEBUG(absl::StrFormat("consumer:%s is not in route's:%s allow_set",
consumer.name, route_name));
goto failed;
}
if (!rule.global_credentials) {
auto audience_json = token.get_payload_claim("aud");
if (audience_json.get_type() != jwt::json::type::string) {
LOG_DEBUG(absl::StrFormat("invalid audience, token:%s", token_str));
goto failed;
}
auto audience = audience_json.as_string();
if (audience != route_name) {
LOG_DEBUG(absl::StrFormat("audience:%s not match this route:%s",
audience, route_name));
goto failed;
}
}
if (!rule.keep_token) {
removeRequestHeader(rule.auth_header_name);
}
addRequestHeader("X-Mse-Consumer", consumer.name);
return true;
}
failed:
if (!verified) {
auto authn_value = absl::StrCat(
"Bearer realm=\"",
Wasm::Common::Http::buildOriginalUri(MaximumUriLength), "\"");
sendLocalResponse(401, kRcDetailOAuthPrefix, "Invalid Jwt token",
{{"WWW-Authenticate", authn_value}});
} else {
sendLocalResponse(403, kRcDetailOAuthPrefix, "Access Denied", {});
}
return false;
}
bool PluginRootContext::onConfigure(size_t size) {
// Parse configuration JSON string.
if (size > 0 && !configure(size)) {
LOG_WARN("configuration has errors initialization will not continue.");
setInvalidConfig();
return false;
}
return true;
}
bool PluginRootContext::configure(size_t configuration_size) {
auto configuration_data = getBufferBytes(WasmBufferType::PluginConfiguration,
0, configuration_size);
// Parse configuration JSON string.
auto result = ::Wasm::Common::JsonParse(configuration_data->view());
if (!result) {
LOG_WARN(absl::StrCat("cannot parse plugin configuration JSON string: ",
configuration_data->view()));
return false;
}
if (!parseAuthRuleConfig(result.value())) {
LOG_WARN(absl::StrCat("cannot parse plugin configuration JSON string: ",
configuration_data->view()));
return false;
}
return true;
}
FilterHeadersStatus PluginContext::onRequestHeaders(uint32_t, bool) {
auto* rootCtx = rootContext();
auto config = rootCtx->getMatchAuthConfig();
if (!config.first) {
return FilterHeadersStatus::Continue;
}
config_ = config.first;
getValue({"route_name"}, &route_name_);
auto path = getRequestHeader(Wasm::Common::Http::Header::Path)->toString();
auto params_pos = path.find('?');
size_t uri_end;
if (params_pos == std::string::npos) {
uri_end = path.size();
} else {
uri_end = params_pos;
}
// Authorize request
if (absl::EndsWith({path.c_str(), uri_end},
config_.value().get().auth_path)) {
std::string err_msg, token;
auto method =
getRequestHeader(Wasm::Common::Http::Header::Method)->toString();
if (method == "GET") {
if (params_pos == std::string::npos) {
err_msg = "Authorize parameters are missing";
goto done;
}
params_pos++;
rootCtx->generateToken(
config_.value(), route_name_,
{path.c_str() + params_pos, path.size() - params_pos}, &token,
&err_msg);
goto done;
}
if (method == "POST") {
auto content_type =
getRequestHeader(Wasm::Common::Http::Header::ContentType)->toString();
if (!absl::StrContains(absl::AsciiStrToLower(content_type),
"application/x-www-form-urlencoded")) {
err_msg = "Invalid content-type";
goto done;
}
check_body_params_ = true;
}
done:
if (!err_msg.empty()) {
sendLocalResponse(400, generateRcDetails(err_msg), err_msg, {});
return FilterHeadersStatus::StopIteration;
}
if (!token.empty()) {
sendLocalResponse(200, "",
absl::StrFormat(TokenResponseTemplate, token,
config_.value().get().token_ttl),
{{"Content-Type", "application/json"}});
}
return FilterHeadersStatus::Continue;
}
return rootCtx->checkAuthRule(
[rootCtx, this](const auto& config, const auto& allow_set) {
return rootCtx->checkPlugin(config, allow_set, route_name_);
})
? FilterHeadersStatus::Continue
: FilterHeadersStatus::StopIteration;
}
FilterDataStatus PluginContext::onRequestBody(size_t body_size,
bool end_stream) {
if (!check_body_params_) {
return FilterDataStatus::Continue;
}
body_total_size_ += body_size;
if (!end_stream) {
return FilterDataStatus::StopIterationAndBuffer;
}
auto* rootCtx = rootContext();
auto body =
getBufferBytes(WasmBufferType::HttpRequestBody, 0, body_total_size_);
LOG_DEBUG(absl::StrFormat("authorize request body: %s", body->toString()));
std::string token, err_msg;
if (rootCtx->generateToken(config_.value(), route_name_, body->view(), &token,
&err_msg)) {
sendLocalResponse(200, "",
absl::StrFormat(TokenResponseTemplate, token,
config_.value().get().token_ttl),
{{"Content-Type", "application/json"}});
return FilterDataStatus::Continue;
}
sendLocalResponse(400, generateRcDetails(err_msg), err_msg, {});
return FilterDataStatus::StopIterationNoBuffer;
}
#ifdef NULL_PLUGIN
} // namespace oauth
} // namespace null_plugin
} // namespace proxy_wasm
#endif

View File

@@ -0,0 +1,105 @@
/*
* Copyright (c) 2022 Alibaba Group Holding Ltd.
*
* 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.
*/
#include <assert.h>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include "common/route_rule_matcher.h"
#include "jwt-cpp/jwt.h"
#define ASSERT(_X) assert(_X)
#ifndef NULL_PLUGIN
#include "proxy_wasm_intrinsics.h"
#else
#include "include/proxy-wasm/null_plugin.h"
namespace proxy_wasm {
namespace null_plugin {
namespace oauth {
#endif
struct Consumer {
std::string name;
std::string client_id;
std::string client_secret;
};
struct OAuthConfigRule {
std::unordered_map<std::string, Consumer> consumers;
std::string issuer = "Higress-Gateway";
std::string auth_header_name = "Authorization";
std::string auth_path = "/oauth2/token";
bool global_credentials = true;
uint64_t token_ttl = 7200;
bool keep_token = true;
uint64_t clock_skew = 60;
};
// PluginRootContext is the root context for all streams processed by the
// thread. It has the same lifetime as the worker thread and acts as target for
// interactions that outlives individual stream, e.g. timer, async calls.
class PluginRootContext : public RootContext,
public RouteRuleMatcher<OAuthConfigRule> {
public:
PluginRootContext(uint32_t id, std::string_view root_id)
: RootContext(id, root_id) {}
~PluginRootContext() {}
bool onConfigure(size_t) override;
bool checkPlugin(const OAuthConfigRule&,
const std::optional<std::unordered_set<std::string>>&,
const std::string&);
bool configure(size_t);
bool generateToken(const OAuthConfigRule& rule, const std::string& route_name,
const absl::string_view& raw_params, std::string* token,
std::string* err_msg);
private:
bool parsePluginConfig(const json&, OAuthConfigRule&) override;
};
// Per-stream context.
class PluginContext : public Context {
public:
explicit PluginContext(uint32_t id, RootContext* root) : Context(id, root) {}
FilterHeadersStatus onRequestHeaders(uint32_t, bool) override;
FilterDataStatus onRequestBody(size_t, bool) override;
private:
inline PluginRootContext* rootContext() {
return dynamic_cast<PluginRootContext*>(this->root());
}
std::string route_name_;
std::optional<std::reference_wrapper<OAuthConfigRule>> config_;
bool check_body_params_ = false;
size_t body_total_size_ = 0;
};
#ifdef NULL_PLUGIN
} // namespace oauth
} // namespace null_plugin
} // namespace proxy_wasm
#endif

View File

@@ -0,0 +1,478 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// 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.
#include "extensions/oauth/plugin.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "include/proxy-wasm/context.h"
#include "include/proxy-wasm/null.h"
namespace proxy_wasm {
namespace null_plugin {
namespace oauth {
NullPluginRegistry* context_registry_;
RegisterNullVmPluginFactory register_oauth_plugin("oauth", []() {
return std::make_unique<NullPlugin>(oauth::context_registry_);
});
class MockContext : public proxy_wasm::ContextBase {
public:
MockContext(WasmBase* wasm) : ContextBase(wasm) {}
MOCK_METHOD(BufferInterface*, getBuffer, (WasmBufferType));
MOCK_METHOD(WasmResult, log, (uint32_t, std::string_view));
MOCK_METHOD(WasmDataPtr, getBufferBytes, (WasmBufferType, size_t, size_t));
MOCK_METHOD(WasmResult, getHeaderMapPairs, (WasmHeaderMapType, Pairs*));
MOCK_METHOD(WasmResult, getHeaderMapValue,
(WasmHeaderMapType /* type */, std::string_view /* jwt */,
std::string_view* /*result */));
MOCK_METHOD(WasmResult, addHeaderMapValue,
(WasmHeaderMapType /* type */, std::string_view /* jwt */,
std::string_view /* value */));
MOCK_METHOD(WasmResult, sendLocalResponse,
(uint32_t /* response_code */, std::string_view /* body */,
Pairs /* additional_headers */, uint32_t /* grpc_status */,
std::string_view /* details */));
MOCK_METHOD(uint64_t, getCurrentTimeNanoseconds, ());
MOCK_METHOD(WasmResult, getProperty, (std::string_view, std::string*));
MOCK_METHOD(WasmResult, httpCall,
(std::string_view, const Pairs&, std::string_view, const Pairs&,
int, uint32_t*));
};
class OAuthTest : public ::testing::Test {
protected:
OAuthTest() {
// Initialize test VM
test_vm_ = createNullVm();
wasm_base_ = std::make_unique<WasmBase>(
std::move(test_vm_), "test-vm", "", "",
std::unordered_map<std::string, std::string>{},
AllowedCapabilitiesMap{});
wasm_base_->load("oauth");
wasm_base_->initialize();
// Initialize host side context
mock_context_ = std::make_unique<MockContext>(wasm_base_.get());
current_context_ = mock_context_.get();
ON_CALL(*mock_context_, log(testing::_, testing::_))
.WillByDefault([](uint32_t, std::string_view m) {
std::cerr << m << "\n";
return WasmResult::Ok;
});
ON_CALL(*mock_context_, getHeaderMapValue(WasmHeaderMapType::RequestHeaders,
testing::_, testing::_))
.WillByDefault([&](WasmHeaderMapType, std::string_view header,
std::string_view* result) {
if (header == ":authority") {
*result = authority_;
}
if (header == ":path") {
*result = path_;
}
if (header == ":method") {
*result = method_;
}
if (header == "Authorization") {
*result = jwt_header_;
}
if (header == "content-type") {
*result = content_type_;
}
if (header == "x-custom-header") {
*result = custom_header_;
}
return WasmResult::Ok;
});
ON_CALL(*mock_context_, addHeaderMapValue(WasmHeaderMapType::RequestHeaders,
testing::_, testing::_))
.WillByDefault([&](WasmHeaderMapType, std::string_view jwt,
std::string_view value) { return WasmResult::Ok; });
ON_CALL(*mock_context_, getCurrentTimeNanoseconds()).WillByDefault([&]() {
return current_time_;
});
ON_CALL(*mock_context_, getProperty(testing::_, testing::_))
.WillByDefault([&](std::string_view path, std::string* result) {
*result = route_name_;
return WasmResult::Ok;
});
ON_CALL(*mock_context_, getBufferBytes(WasmBufferType::HttpCallResponseBody,
testing::_, testing::_))
.WillByDefault([&](WasmBufferType, size_t, size_t) {
return std::make_unique<WasmData>(http_call_body_.data(),
http_call_body_.size());
});
ON_CALL(*mock_context_,
getHeaderMapPairs(WasmHeaderMapType::HttpCallResponseHeaders,
testing::_))
.WillByDefault([&](WasmHeaderMapType, Pairs* result) {
*result = http_call_headers_;
return WasmResult::Ok;
});
ON_CALL(*mock_context_, httpCall(testing::_, testing::_, testing::_,
testing::_, testing::_, testing::_))
.WillByDefault([&](std::string_view, const Pairs&, std::string_view,
const Pairs&, int, uint32_t* token_ptr) {
root_context_->onHttpCallResponse(
*token_ptr, http_call_headers_.size(), http_call_body_.size(), 0);
return WasmResult::Ok;
});
// Initialize Wasm sandbox context
root_context_ = std::make_unique<PluginRootContext>(0, "");
context_ = std::make_unique<PluginContext>(1, root_context_.get());
}
~OAuthTest() override {}
std::unique_ptr<WasmBase> wasm_base_;
std::unique_ptr<WasmVm> test_vm_;
std::unique_ptr<MockContext> mock_context_;
std::unique_ptr<PluginRootContext> root_context_;
std::unique_ptr<PluginContext> context_;
std::string path_;
std::string method_;
std::string authority_;
std::string route_name_;
std::string jwt_header_;
std::string custom_header_;
std::string content_type_;
uint64_t current_time_;
Pairs http_call_headers_;
std::string http_call_body_;
};
TEST_F(OAuthTest, generateToken) {
std::string configuration = R"(
{
"consumers": [
{
"name": "consumer1",
"client_id": "9515b564-0b1d-11ee-9c4c-00163e1250b5",
"client_secret": "9e55de56-0b1d-11ee-b8ec-00163e1250b5"
}
],
"auth_path": "test/token"
})";
BufferBase buffer;
buffer.set({configuration.data(), configuration.size()});
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
EXPECT_TRUE(root_context_->configure(configuration.size()));
path_ = "/abc/test/token";
method_ = "GET";
EXPECT_CALL(*mock_context_,
sendLocalResponse(
400, std::string_view("Authorize parameters are missing"),
testing::_, testing::_, testing::_));
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopIteration);
path_ = "/abc/test/token?";
method_ = "GET";
EXPECT_CALL(*mock_context_,
sendLocalResponse(400, std::string_view("grant_type is missing"),
testing::_, testing::_, testing::_));
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopIteration);
path_ =
"/abc/test/"
"token?grant_type=client_credentials";
method_ = "GET";
EXPECT_CALL(*mock_context_,
sendLocalResponse(400, std::string_view("client_id is missing"),
testing::_, testing::_, testing::_));
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopIteration);
path_ =
"/abc/test/"
"token?grant_type=client_credentials&client_id=9515b564-0b1d-11ee-9c4c-"
"00163e1250b5&client_secret=abcd";
method_ = "GET";
EXPECT_CALL(*mock_context_,
sendLocalResponse(
400, std::string_view("invalid client_id or client_secret"),
testing::_, testing::_, testing::_));
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopIteration);
path_ =
"/abc/test/"
"token?grant_type=client_credentials&client_id=9515b564-0b1d-11ee-9c4c-"
"00163e1250b5&client_secret=9e55de56-0b1d-11ee-b8ec-00163e1250b5";
method_ = "GET";
EXPECT_CALL(*mock_context_, sendLocalResponse(200, testing::_, testing::_,
testing::_, testing::_));
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::Continue);
path_ = "/abc/test/token";
method_ = "POST";
content_type_ = "application/x-www-form-urlencoded; charset=utf8";
std::string body = "grant_type=client_credentials&client_id=wrongid";
BufferBase body_buffer;
body_buffer.set({body.data(), body.size()});
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::Continue);
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::HttpRequestBody))
.WillOnce([&body_buffer](WasmBufferType) { return &body_buffer; });
EXPECT_CALL(*mock_context_,
sendLocalResponse(
400, std::string_view("invalid client_id or client_secret"),
testing::_, testing::_, testing::_));
EXPECT_EQ(context_->onRequestBody(body.size(), true),
FilterDataStatus::StopIterationNoBuffer);
path_ = "/abc/test/token";
method_ = "POST";
content_type_ = "application/x-www-form-urlencoded; charset=utf8";
body =
"grant_type=client_credentials&client_id=9515b564-0b1d-11ee-9c4c-"
"00163e1250b5&client_secret=9e55de56-0b1d-11ee-b8ec-00163e1250b5";
body_buffer;
body_buffer.set({body.data(), body.size()});
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::Continue);
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::HttpRequestBody))
.WillOnce([&body_buffer](WasmBufferType) { return &body_buffer; });
EXPECT_CALL(*mock_context_, sendLocalResponse(200, testing::_, testing::_,
testing::_, testing::_));
EXPECT_EQ(context_->onRequestBody(body.size(), true),
FilterDataStatus::Continue);
}
TEST_F(OAuthTest, invalidToken) {
std::string configuration = R"(
{
"consumers": [
{
"name": "consumer1",
"client_id": "9515b564-0b1d-11ee-9c4c-00163e1250b5",
"client_secret": "9e55de56-0b1d-11ee-b8ec-00163e1250b5"
}
]
})";
BufferBase buffer;
buffer.set({configuration.data(), configuration.size()});
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
EXPECT_TRUE(root_context_->configure(configuration.size()));
jwt_header_ = R"(Bearer alksdjf)";
EXPECT_CALL(*mock_context_, sendLocalResponse(401, testing::_, testing::_,
testing::_, testing::_));
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopIteration);
jwt_header_ = R"(alksdjf)";
EXPECT_CALL(*mock_context_, sendLocalResponse(401, testing::_, testing::_,
testing::_, testing::_));
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopIteration);
jwt_header_ =
R"(Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6ImFwcGxpY2F0aW9uL2F0K2p3dCJ9.eyJhdWQiOiJkZWZhdWx0IiwiZXhwIjoxNjY1NjczODI5LCJpYXQiOjE2NjU2NzM4MTksImlzcyI6IkhpZ3Jlc3MtR2F0ZXdheSIsImp0aSI6IjEwOTU5ZDFiLThkNjEtNGRlYy1iZWE3LTk0ODEwMzc1YjYzYyIsInNjb3BlIjoidGVzdCIsInN1YiI6ImNvbnN1bWVyMiJ9.al7eoRdoNQlNx8HCqNesj7woiLOJmJLSqnZ)";
EXPECT_CALL(*mock_context_, sendLocalResponse(401, testing::_, testing::_,
testing::_, testing::_));
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopIteration);
}
TEST_F(OAuthTest, expire) {
std::string configuration = R"(
{
"consumers": [
{
"name": "consumer1",
"client_id": "9515b564-0b1d-11ee-9c4c-00163e1250b5",
"client_secret": "9e55de56-0b1d-11ee-b8ec-00163e1250b5"
}
]
})";
BufferBase buffer;
buffer.set({configuration.data(), configuration.size()});
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
EXPECT_TRUE(root_context_->configure(configuration.size()));
jwt_header_ =
R"(Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6ImFwcGxpY2F0aW9uL2F0K2p3dCJ9.eyJhdWQiOiJ0ZXN0MiIsImNsaWVudF9pZCI6Ijk1MTViNTY0LTBiMWQtMTFlZS05YzRjLTAwMTYzZTEyNTBiNSIsImV4cCI6MTY2NTY3MzgyOSwiaWF0IjoxNjY1NjczODE5LCJpc3MiOiJIaWdyZXNzLUdhdGV3YXkiLCJqdGkiOiIxMDk1OWQxYi04ZDYxLTRkZWMtYmVhNy05NDgxMDM3NWI2M2MiLCJzY29wZSI6InRlc3QiLCJzdWIiOiJjb25zdW1lcjEifQ.LsZ6mlRxlaqWa0IAZgmGVuDgypRbctkTcOyoCxqLrHY)";
route_name_ = "test2";
EXPECT_CALL(*mock_context_, sendLocalResponse(401, testing::_, testing::_,
testing::_, testing::_));
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopIteration);
}
TEST_F(OAuthTest, routeAuth) {
std::string configuration = R"(
{
"consumers": [
{
"name": "consumer1",
"client_id": "9515b564-0b1d-11ee-9c4c-00163e1250b5",
"client_secret": "9e55de56-0b1d-11ee-b8ec-00163e1250b5"
}
],
"global_credentials": false,
"clock_skew_seconds": 3153600000
})";
BufferBase buffer;
buffer.set({configuration.data(), configuration.size()});
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
EXPECT_TRUE(root_context_->configure(configuration.size()));
jwt_header_ =
R"(Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6ImFwcGxpY2F0aW9uL2F0K2p3dCJ9.eyJhdWQiOiJ0ZXN0MiIsImNsaWVudF9pZCI6Ijk1MTViNTY0LTBiMWQtMTFlZS05YzRjLTAwMTYzZTEyNTBiNSIsImV4cCI6MTY2NTY3MzgyOSwiaWF0IjoxNjY1NjczODE5LCJpc3MiOiJIaWdyZXNzLUdhdGV3YXkiLCJqdGkiOiIxMDk1OWQxYi04ZDYxLTRkZWMtYmVhNy05NDgxMDM3NWI2M2MiLCJzY29wZSI6InRlc3QiLCJzdWIiOiJjb25zdW1lcjEifQ.LsZ6mlRxlaqWa0IAZgmGVuDgypRbctkTcOyoCxqLrHY)";
EXPECT_CALL(*mock_context_, sendLocalResponse(403, testing::_, testing::_,
testing::_, testing::_));
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopIteration);
route_name_ = "test2";
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::Continue);
}
TEST_F(OAuthTest, globalAuth) {
std::string configuration = R"(
{
"consumers": [
{
"name": "consumer1",
"client_id": "9515b564-0b1d-11ee-9c4c-00163e1250b5",
"client_secret": "9e55de56-0b1d-11ee-b8ec-00163e1250b5"
}
],
"clock_skew_seconds": 3153600000
})";
BufferBase buffer;
buffer.set({configuration.data(), configuration.size()});
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
EXPECT_TRUE(root_context_->configure(configuration.size()));
jwt_header_ =
R"(Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6ImFwcGxpY2F0aW9uL2F0K2p3dCJ9.eyJhdWQiOiJ0ZXN0MiIsImNsaWVudF9pZCI6Ijk1MTViNTY0LTBiMWQtMTFlZS05YzRjLTAwMTYzZTEyNTBiNSIsImV4cCI6MTY2NTY3MzgyOSwiaWF0IjoxNjY1NjczODE5LCJpc3MiOiJIaWdyZXNzLUdhdGV3YXkiLCJqdGkiOiIxMDk1OWQxYi04ZDYxLTRkZWMtYmVhNy05NDgxMDM3NWI2M2MiLCJzY29wZSI6InRlc3QiLCJzdWIiOiJjb25zdW1lcjEifQ.LsZ6mlRxlaqWa0IAZgmGVuDgypRbctkTcOyoCxqLrHY)";
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::Continue);
}
TEST_F(OAuthTest, AuthZ) {
std::string configuration = R"(
{
"consumers": [
{
"name": "consumer1",
"client_id": "9515b564-0b1d-11ee-9c4c-00163e1250b5",
"client_secret": "9e55de56-0b1d-11ee-b8ec-00163e1250b5"
},
{
"name": "consumer2",
"client_id": "d001d242-0bf0-11ee-97cb-00163e1250b5",
"client_secret": "d60bdafc-0bf0-11ee-afba-00163e1250b5"
}
],
"clock_skew_seconds": 3153600000,
"global_credentials": true,
"_rules_": [
{
"_match_route_": [
"test1"
],
"allow": [
"consumer2"
]
},
{
"_match_route_": [
"test2"
],
"allow": [
"consumer1"
]
}
]
})";
BufferBase buffer;
buffer.set({configuration.data(), configuration.size()});
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
EXPECT_TRUE(root_context_->configure(configuration.size()));
jwt_header_ =
R"(Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6ImFwcGxpY2F0aW9uL2F0K2p3dCJ9.eyJhdWQiOiJ0ZXN0MiIsImNsaWVudF9pZCI6Ijk1MTViNTY0LTBiMWQtMTFlZS05YzRjLTAwMTYzZTEyNTBiNSIsImV4cCI6MTY2NTY3MzgyOSwiaWF0IjoxNjY1NjczODE5LCJpc3MiOiJIaWdyZXNzLUdhdGV3YXkiLCJqdGkiOiIxMDk1OWQxYi04ZDYxLTRkZWMtYmVhNy05NDgxMDM3NWI2M2MiLCJzY29wZSI6InRlc3QiLCJzdWIiOiJjb25zdW1lcjEifQ.LsZ6mlRxlaqWa0IAZgmGVuDgypRbctkTcOyoCxqLrHY)";
route_name_ = "test1";
EXPECT_CALL(*mock_context_, sendLocalResponse(403, testing::_, testing::_,
testing::_, testing::_));
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopIteration);
route_name_ = "test2";
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::Continue);
jwt_header_ =
R"(Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6ImFwcGxpY2F0aW9uL2F0K2p3dCJ9.eyJhdWQiOiJkZWZhdWx0IiwiY2xpZW50X2lkIjoiZDAwMWQyNDItMGJmMC0xMWVlLTk3Y2ItMDAxNjNlMTI1MGI1IiwiZXhwIjoxNjY1NjczODI5LCJpYXQiOjE2NjU2NzM4MTksImlzcyI6IkhpZ3Jlc3MtR2F0ZXdheSIsImp0aSI6IjEwOTU5ZDFiLThkNjEtNGRlYy1iZWE3LTk0ODEwMzc1YjYzYyIsInNjb3BlIjoidGVzdCIsInN1YiI6ImNvbnN1bWVyMiJ9.whS5U7llGX2BNAX19mjyxiWXa7wVs0_ONVByKVR9ntM)";
route_name_ = "test2";
EXPECT_CALL(*mock_context_, sendLocalResponse(403, testing::_, testing::_,
testing::_, testing::_));
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopIteration);
route_name_ = "test1";
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::Continue);
}
TEST_F(OAuthTest, EmptyConsumer) {
std::string configuration = R"(
{
"consumers": [
],
"_rules_": [
{
"_match_route_": [
"test1"
],
"allow": [
]
}
]
})";
BufferBase buffer;
buffer.set({configuration.data(), configuration.size()});
EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration))
.WillOnce([&buffer](WasmBufferType) { return &buffer; });
EXPECT_TRUE(root_context_->configure(configuration.size()));
jwt_header_ =
R"(Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6ImFwcGxpY2F0aW9uL2F0K2p3dCJ9.eyJhdWQiOiJ0ZXN0MiIsImNsaWVudF9pZCI6Ijk1MTViNTY0LTBiMWQtMTFlZS05YzRjLTAwMTYzZTEyNTBiNSIsImV4cCI6MTY2NTY3MzgyOSwiaWF0IjoxNjY1NjczODE5LCJpc3MiOiJIaWdyZXNzLUdhdGV3YXkiLCJqdGkiOiIxMDk1OWQxYi04ZDYxLTRkZWMtYmVhNy05NDgxMDM3NWI2M2MiLCJzY29wZSI6InRlc3QiLCJzdWIiOiJjb25zdW1lcjEifQ.LsZ6mlRxlaqWa0IAZgmGVuDgypRbctkTcOyoCxqLrHY)";
route_name_ = "test1";
EXPECT_CALL(*mock_context_, sendLocalResponse(401, testing::_, testing::_,
testing::_, testing::_));
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::StopIteration);
route_name_ = "test2";
EXPECT_EQ(context_->onRequestHeaders(0, false),
FilterHeadersStatus::Continue);
}
} // namespace oauth
} // namespace null_plugin
} // namespace proxy_wasm

View File

@@ -0,0 +1,122 @@
# 功能说明
`key-auth`插件实现了基于 API Key 进行认证鉴权的功能,支持从 HTTP 请求的 URL 参数或者请求头解析 API Key同时验证该 API Key 是否有权限访问。
# 配置字段
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
| ----------- | --------------- | ------------------------------------------- | ------ | ----------------------------------------------------------- |
| `global_auth` | bool | 选填 | - | 若配置为true则全局生效认证机制; 若配置为false则只对做了配置的域名和路由生效认证机制; 若不配置则仅当没有域名和路由配置时全局生效(兼容机制) |
| `consumers` | array of object | 必填 | - | 配置服务的调用者,用于对请求进行认证 |
| `keys` | array of string | 必填 | - | API Key 的来源字段名称,可以是 URL 参数或者 HTTP 请求头名称 |
| `in_query` | bool | `in_query``in_header` 至少有一个为 true | true | 配置 true 时,网关会尝试从 URL 参数中解析 API Key |
| `in_header` | bool | `in_query``in_header` 至少有一个为 true | true | 配置 true 时,网关会尝试从 HTTP 请求头中解析 API Key |
`consumers`中每一项的配置字段说明如下:
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
| ------------ | -------- | -------- | ------ | ------------------------ |
| `credential` | string | 必填 | - | 配置该consumer的访问凭证 |
| `name` | string | 必填 | - | 配置该consumer的名称 |
**注意:**
- 对于通过认证鉴权的请求请求的header会被添加一个`X-Mse-Consumer`字段,用以标识调用者的名称。
# 配置示例
## 对特定路由或域名开启
以下配置将对网关特定路由或域名开启 Key Auth 认证和鉴权,注意`credential`字段不能重复
```yaml
global_auth: true
consumers:
- credential: 2bda943c-ba2b-11ec-ba07-00163e1250b5
name: consumer1
- credential: c8c8e9ca-558e-4a2d-bb62-e700dcc40e35
name: consumer2
keys:
- apikey
- x-api-key
```
**路由级配置**
对 route-a 和 route-b 这两个路由做如下配置:
```yaml
allow:
- consumer1
```
对 *.example.com 和 test.com 在这两个域名做如下配置:
```yaml
allow:
- consumer2
```
### 根据该配置,下列请求可以允许访问:
假设以下请求会匹配到route-a这条路由
**将 API Key 设置在 url 参数中**
```bash
curl http://xxx.hello.com/test?apikey=2bda943c-ba2b-11ec-ba07-00163e1250b5
```
**将 API Key 设置在 http 请求头中**
```bash
curl http://xxx.hello.com/test -H 'x-api-key: 2bda943c-ba2b-11ec-ba07-00163e1250b5'
```
认证鉴权通过后请求的header中会被添加一个`X-Mse-Consumer`字段,在此例中其值为`consumer1`,用以标识调用方的名称
### 下列请求将拒绝访问:
**请求未提供 API Key返回401**
```bash
curl http://xxx.hello.com/test
```
**请求提供的 API Key 无权访问返回401**
```bash
curl http://xxx.hello.com/test?apikey=926d90ac-ba2e-11ec-ab68-00163e1250b5
```
**根据请求提供的 API Key匹配到的调用者无访问权限返回403**
```bash
# consumer2不在route-a的allow列表里
curl http://xxx.hello.com/test?apikey=c8c8e9ca-558e-4a2d-bb62-e700dcc40e35
```
## 网关实例级别开启
以下配置未指定`matchRules`字段,因此将对网关实例级别开启全局 Key Auth 认证.
```yaml
defaultConfig
consumers:
- credential: 2bda943c-ba2b-11ec-ba07-00163e1250b5
name: consumer1
- credential: c8c8e9ca-558e-4a2d-bb62-e700dcc40e35
name: consumer2
keys:
- apikey
in_query: true
```
开启`matchRules`方式如下:
```yaml
matchRules:
- config:
allow:
- consumer1
```
# 相关错误码
| HTTP 状态码 | 出错信息 | 原因说明 |
| ----------- | --------------------------------------------------------- | ----------------------- |
| 401 | Request denied by Key Auth check. Muti API key found in request | 请求提供多个 API Key |
| 401 | Request denied by Key Auth check. No API key found in request | 请求未提供 API Key |
| 401 | Request denied by Key Auth check. Invalid API key | 不允许当前 API Key 访问 |
| 403 | Request denied by Key Auth check. Unauthorized consumer | 请求的调用方无访问权限 |

View File

@@ -0,0 +1,109 @@
# Features
The `key-auth` plug-in implements the authentication function based on the API Key, supports parsing the API Key from the URL parameter or request header of the HTTP request, and verifies whether the API Key has permission to access.
# Configuration field
| Name | Data Type | Parameter requirements | Default| Description |
| ----------- | --------------- | -------------------------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------- |
| `global_auth` | bool | Optional | - | If configured to true, the authentication mechanism will take effect globally; if configured to false, the authentication mechanism will only take effect for the configured domain names and routes; if not configured, the authentication mechanism will only take effect globally when no domain names and routes are configured (compatibility mechanism) |
| `consumers` | array of object | Required | - | Configure the caller of the service to authenticate the request. |
| `keys` | array of string | Required | - | The name of the source field of the API Key, which can be a URL parameter or an HTTP request header name. |
| `in_query` | bool | At least one of `in_query` and `in_header` must be true. | true | When configured true, the gateway will try to parse the API Key from the URL parameters. |
| `in_header` | bool | The same as above. | true | The same as above. |
The configuration fields of each item in `consumers` are described as follows:
| Name | Data Type | Parameter requirements | Default | Description |
| ------------ | --------- | -----------------------| ------ | ------------------------------------------- |
| `credential` | string | Required | - | Configure the consumer's access credentials. |
| `name` | string | Required | - | Configure the name of the consumer. |
**Warning**
- For a request that passes authentication, an `X-Mse-Consumer` field will be added to the request header to identify the name of the caller.
# Example configuration
## Enabled for specific routes or domains
The following configuration will enable Key Auth authentication and authentication for gateway-specific routes or domain names. Note that the `credential` field can not be repeated.
```yaml
global_auth: true
consumers:
- credential: 2bda943c-ba2b-11ec-ba07-00163e1250b5
name: consumer1
- credential: c8c8e9ca-558e-4a2d-bb62-e700dcc40e35
name: consumer2
keys:
- apikey
- x-api-key
in_query: true
```
The `route-a` and `route-b` specified in `_match_route_` in this example are the route names filled in when creating the gateway route. When these two routes are matched, calls whose `name` is `consumer1` will be allowed Access by callers, other callers are not allowed to access;
`*.example.com` and `test.com` specified in `_match_domain_` in this example are used to match the domain name of the request. When the domain name matches, the caller whose `name` is `consumer2` will be allowed to access, and other calls access is not allowed.
### Depending on this configuration, the following requests would allow access
Assume that the following request will match the route-a route:
**Set the API Key in the url parameter**
```bash
curl http://xxx.hello.com/test?apikey=2bda943c-ba2b-11ec-ba07-00163e1250b5
```
**Set the API Key in the http request header**
```bash
curl http://xxx.hello.com/test -H 'x-api-key: 2bda943c-ba2b-11ec-ba07-00163e1250b5'
```
After the authentication is passed, an `X-Mse-Consumer` field will be added to the header of the request. In this example, its value is `consumer1`, which is used to identify the name of the caller.
### The following requests will deny access
**The request does not provide an API Key, return 401**
```bash
curl http://xxx.hello.com/test
```
**The API Key provided by the request is not authorized to access, return 401**
```bash
curl http://xxx.hello.com/test?apikey=926d90ac-ba2e-11ec-ab68-00163e1250b5
```
**The caller matched according to the API Key provided in the request has no access rights, return 403**
```bash
# consumer2 is not in the allow list of route-a
curl http://xxx.hello.com/test?apikey=c8c8e9ca-558e-4a2d-bb62-e700dcc40e35
```
## Gateway instance level enabled
The following configuration does not specify the `matchRules` field, so Key Auth authentication will be enabled at the gateway instance level.
```yaml
consumers:
- credential: 2bda943c-ba2b-11ec-ba07-00163e1250b5
name: consumer1
- credential: c8c8e9ca-558e-4a2d-bb62-e700dcc40e35
name: consumer2
keys:
- apikey
in_query: true
```
configuration specify the `matchRules` field, like
```yaml
matchRules:
- config:
allow:
- consumer1
```
# Error code
| HTTP status code | Error information | Reason |
| ---------------- | --------------------------------------------------------- | -------------------------------------------- |
| 401 | Muti API key found in request. | Muti API provided by request Key. |
| 401 | No API key found in request. | API not provided by request Key. |
| 401 | Request denied by Key Auth check. Invalid API key. | Current API Key access is not allowed. |
| 403 | Request denied by Key Auth check. Unauthorized consumer. | The requested caller does not have access. |

View File

@@ -0,0 +1,19 @@
module key-auth
go 1.19
replace github.com/alibaba/higress/plugins/wasm-go => ../..
require (
github.com/alibaba/higress/plugins/wasm-go v0.0.0-20231017105619-a18879bf867c
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0
github.com/tidwall/gjson v1.14.4
)
require (
github.com/google/uuid v1.3.0 // indirect
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
github.com/magefile/mage v1.14.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
)

View File

@@ -0,0 +1,18 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0 h1:kS7BvMKN+FiptV4pfwiNX8e3q14evxAWkhYbxt8EI1M=
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0/go.mod h1:qkW5MBz2jch2u8bS59wws65WC+Gtx3x0aPUX5JL7CXI=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -0,0 +1,62 @@
apiVersion: networking.higress.io/v1
kind: McpBridge
metadata:
name: mcp-keyauth-httpbin
namespace: higress-system
spec:
registries:
- domain: httpbin.org
name: httpbin
port: 80
type: dns
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
higress.io/destination: httpbin.dns
higress.io/upstream-vhost: "httpbin.org"
higress.io/backend-protocol: HTTP
name: ingress-keyauth-httpbin
namespace: higress-system
spec:
ingressClassName: higress
rules:
- host: httpbin.example.com
http:
paths:
- backend:
resource:
apiGroup: networking.higress.io
kind: McpBridge
name: mcp-keyauth-httpbin
path: /
pathType: Prefix
---
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
name: wasm-keyauth-httpbin
namespace: higress-system
spec:
defaultConfig:
consumers:
- credential: 2bda943c-ba2b-11ec-ba07-00163e1250b5
name: consumer1
- credential: c8c8e9ca-558e-4a2d-bb62-e700dcc40e35
name: consumer2
global_auth: false
keys:
- x-api-key
- apikey
in_header: true
defaultConfigDisable: false
matchRules:
- config:
allow:
- consumer1
configDisable: false
ingress:
- ingress-keyauth-httpbin
url: oci://docker.io/dongjiang1989/keyauth:1.0.0
imagePullPolicy: Always

View File

@@ -0,0 +1,357 @@
// Copyright (c) 2023 Alibaba Group Holding Ltd.
//
// 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 main
import (
"errors"
"fmt"
"net/url"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
"github.com/tidwall/gjson"
)
var (
ruleSet bool // 插件是否至少在一个 domain 或 route 上生效
protectionSpace = "MSE Gateway" // 认证失败时,返回响应头 WWW-Authenticate: Key realm=MSE Gateway
)
func main() {
wrapper.SetCtx(
"key-auth", // middleware name
wrapper.ParseOverrideConfigBy(parseGlobalConfig, parseOverrideRuleConfig),
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
)
}
type Consumer struct {
// @Title 名称
// @Title en-US Name
// @Description 该调用方的名称。
// @Description en-US The name of the consumer.
Name string `yaml:"name"`
// @Title 访问凭证
// @Title en-US Credential
// @Description 该调用方的访问凭证。
// @Description en-US The credential of the consumer.
// @Scope GLOBAL
Credential string `yaml:"credential"`
}
// @Name key-auth
// @Category auth
// @Phase AUTHN
// @Priority 321
// @Title zh-CN Key Auth
// @Description zh-CN 本插件实现了实现了基于 API Key 进行认证鉴权的功能.
// @Description en-US This plugin implements an authentication function based on API Key Auth standard.
// @IconUrl https://img.alicdn.com/imgextra/i4/O1CN01BPFGlT1pGZ2VDLgaH_!!6000000005333-2-tps-42-42.png
// @Version 1.0.0
//
// @Contact.name Higress Team
// @Contact.url http://higress.io/
// @Contact.email admin@higress.io
//
// @Example
// global_auth: false
// consumers:
// - name: consumer1
// credential: token1
// - name: consumer2
// credential: token2
// keys:
// - x-api-key
// - token
// in_query: true
// @End
type KeyAuthConfig struct {
// @Title 是否开启全局认证
// @Title en-US Enable Global Auth
// @Description 若不开启全局认证,则全局配置只提供凭证信息。只有在域名或路由上进行了配置才会启用认证。
// @Description en-US If set to false, only consumer info will be accepted from the global config. Auth feature shall only be enabled if the corresponding domain or route is configured.
// @Scope GLOBAL
globalAuth *bool `yaml:"global_auth,omitempty"` //是否开启全局认证. 若不开启全局认证,则全局配置只提供凭证信息。只有在域名或路由上进行了配置才会启用认证。
// @Title API Key 的来源字段名称列表
// @Title en-US The name of the source field of the API Key
// @Description API Key 的来源字段名称,可以是 URL 参数或者 HTTP 请求头名称.
// @Description en-US The name of the source field of the API Key, which can be a URL parameter or an HTTP request header name.
// @Scope GLOBAL
Keys []string `yaml:"keys"` // key auth names
// @Title key是否来源于URL参数
// @Title en-US the API Key from the URL parameters.
// @Description 如果配置 true 时,网关会尝试从 URL 参数中解析 API Key
// @Description en-US When configured true, the gateway will try to parse the API Key from the URL parameters.
// @Scope GLOBAL
InQuery bool `yaml:"in_query,omitempty"`
// @Title key是否来源于Header
// @Title en-US the API Key from the HTTP request header name.
// @Description 配置 true 时,网关会尝试从 URL header头中解析 API Key
// @Description en-US When configured true, the gateway will try to parse the API Key from the HTTP request header name.
// @Scope GLOBAL
InHeader bool `yaml:"in_header,omitempty"`
// @Title 调用方列表
// @Title en-US Consumer List
// @Description 服务调用方列表,用于对请求进行认证。
// @Description en-US List of service consumers which will be used in request authentication.
// @Scope GLOBAL
consumers []Consumer `yaml:"consumers"`
// @Title 授权访问的调用方列表
// @Title en-US Allowed Consumers
// @Description 对于匹配上述条件的请求,允许访问的调用方列表。
// @Description en-US Consumers to be allowed for matched requests.
allow []string `yaml:"allow"`
credential2Name map[string]string `yaml:"-"`
}
func parseGlobalConfig(json gjson.Result, global *KeyAuthConfig, log wrapper.Log) error {
log.Debug("global config")
// init
ruleSet = false
global.credential2Name = make(map[string]string)
// global_auth
globalAuth := json.Get("global_auth")
if globalAuth.Exists() {
ga := globalAuth.Bool()
global.globalAuth = &ga
}
// keys
names := json.Get("keys")
if !names.Exists() {
return errors.New("keys is required")
}
if len(names.Array()) == 0 {
return errors.New("keys cannot be empty")
}
for _, name := range names.Array() {
global.Keys = append(global.Keys, name.String())
}
// in_query and in_header
in_query := json.Get("in_query")
in_header := json.Get("in_header")
if !in_query.Exists() && !in_header.Exists() {
return errors.New("must one of in_query/in_header required")
}
if in_query.Exists() {
global.InQuery = in_query.Bool()
}
if in_header.Exists() {
global.InHeader = in_header.Bool()
}
// consumers
consumers := json.Get("consumers")
if !consumers.Exists() {
return errors.New("consumers is required")
}
if len(consumers.Array()) == 0 {
return errors.New("consumers cannot be empty")
}
for _, item := range consumers.Array() {
name := item.Get("name")
if !name.Exists() || name.String() == "" {
return errors.New("consumer name is required")
}
credential := item.Get("credential")
if !credential.Exists() || credential.String() == "" {
return errors.New("consumer credential is required")
}
if _, ok := global.credential2Name[credential.String()]; ok {
return errors.New("duplicate consumer credential: " + credential.String())
}
consumer := Consumer{
Name: name.String(),
Credential: credential.String(),
}
global.consumers = append(global.consumers, consumer)
global.credential2Name[credential.String()] = name.String()
}
return nil
}
func parseOverrideRuleConfig(json gjson.Result, global KeyAuthConfig, config *KeyAuthConfig, log wrapper.Log) error {
log.Debug("domain/route config")
*config = global
allow := json.Get("allow")
if !allow.Exists() {
return errors.New("allow is required")
}
if len(allow.Array()) == 0 {
return errors.New("allow cannot be empty")
}
for _, item := range allow.Array() {
config.allow = append(config.allow, item.String())
}
ruleSet = true
return nil
}
// key-auth 插件认证逻辑:
// - global_auth == true 开启全局生效:
// - 若当前 domain/route 未配置 allow 列表,即未配置该插件:则在所有 consumers 中查找,如果找到则认证通过,否则认证失败 (1*)
// - 若当前 domain/route 配置了该插件:则在 allow 列表中查找,如果找到则认证通过,否则认证失败
//
// - global_auth == false 非全局生效:(2*)
// - 若当前 domain/route 未配置该插件:则直接放行
// - 若当前 domain/route 配置了该插件:则在 allow 列表中查找,如果找到则认证通过,否则认证失败
//
// - global_auth 未设置:
// - 若没有一个 domain/route 配置该插件:则遵循 (1*)
// - 若有至少一个 domain/route 配置该插件:则遵循 (2*)
func onHttpRequestHeaders(ctx wrapper.HttpContext, config KeyAuthConfig, log wrapper.Log) types.Action {
var (
noAllow = len(config.allow) == 0 // 未配置 allow 列表,表示插件在该 domain/route 未生效
globalAuthNoSet = config.globalAuth == nil
globalAuthSetTrue = !globalAuthNoSet && *config.globalAuth
globalAuthSetFalse = !globalAuthNoSet && !*config.globalAuth
)
// 不需要认证而直接放行的情况:
// - global_auth == false 且 当前 domain/route 未配置该插件
// - global_auth 未设置 且 有至少一个 domain/route 配置该插件 且 当前 domain/route 未配置该插件
if globalAuthSetFalse || (globalAuthNoSet && ruleSet) {
if noAllow {
log.Info("authorization is not required")
return types.ActionContinue
}
}
// 以下需要认证:
// - 从 header 中获取 tokens 信息
// - 从 query 中获取 tokens 信息
var tokens []string
if config.InHeader {
// 匹配keys中的 keyname
for _, key := range config.Keys {
value, err := proxywasm.GetHttpRequestHeader(key)
if err == nil && value != "" {
tokens = append(tokens, value)
}
}
} else if config.InQuery {
requestUrl, _ := proxywasm.GetHttpRequestHeader(":path")
url, _ := url.Parse(requestUrl)
queryValues := url.Query()
for _, key := range config.Keys {
values, ok := queryValues[key]
if ok && len(values) > 0 {
tokens = append(tokens, values...)
}
}
}
// header/query
if len(tokens) > 1 {
return deniedMutiKeyAuthData()
} else if len(tokens) <= 0 {
return deniedNoKeyAuthData()
}
// 验证token
name, ok := config.credential2Name[tokens[0]]
if !ok {
log.Warnf("credential %q is not configured", tokens[0])
return deniedUnauthorizedConsumer()
}
// 全局生效:
// - global_auth == true 且 当前 domain/route 未配置该插件
// - global_auth 未设置 且 没有任何一个 domain/route 配置该插件
if (globalAuthSetTrue && noAllow) || (globalAuthNoSet && !ruleSet) {
log.Infof("consumer %q authenticated", name)
return authenticated(name)
}
// 全局生效,但当前 domain/route 配置了 allow 列表
if globalAuthSetTrue && !noAllow {
if !contains(config.allow, name) {
log.Warnf("consumer %q is not allowed", name)
return deniedUnauthorizedConsumer()
}
log.Infof("consumer %q authenticated", name)
return authenticated(name)
}
// 非全局生效
if globalAuthSetFalse || (globalAuthNoSet && ruleSet) {
if !noAllow { // 配置了 allow 列表
if !contains(config.allow, name) {
log.Warnf("consumer %q is not allowed", name)
return deniedUnauthorizedConsumer()
}
log.Infof("consumer %q authenticated", name)
return authenticated(name)
}
}
return types.ActionContinue
}
func deniedMutiKeyAuthData() types.Action {
_ = proxywasm.SendHttpResponse(401, WWWAuthenticateHeader(protectionSpace),
[]byte("Request denied by Key Auth check. Muti Key Authentication information found."), -1)
return types.ActionContinue
}
func deniedNoKeyAuthData() types.Action {
_ = proxywasm.SendHttpResponse(401, WWWAuthenticateHeader(protectionSpace),
[]byte("Request denied by Key Auth check. No Key Authentication information found."), -1)
return types.ActionContinue
}
func deniedUnauthorizedConsumer() types.Action {
_ = proxywasm.SendHttpResponse(403, WWWAuthenticateHeader(protectionSpace),
[]byte("Request denied by Key Auth check. Unauthorized consumer."), -1)
return types.ActionContinue
}
func authenticated(name string) types.Action {
_ = proxywasm.AddHttpRequestHeader("X-Mse-Consumer", name)
return types.ActionContinue
}
func contains(arr []string, item string) bool {
for _, i := range arr {
if i == item {
return true
}
}
return false
}
func WWWAuthenticateHeader(realm string) [][2]string {
return [][2]string{
{"WWW-Authenticate", fmt.Sprintf("Key realm=%s", realm)},
}
}

View File

@@ -1,5 +1,5 @@
# 功能说明
`jwt-auth`插件基于wasm-go实现了Token解析认证功能可以判断Token是否有效如果Token有效则继续访问后端微服务Token无效或不存在直接拒绝并返回401
`simple-jwt-auth`插件基于wasm-go实现了Token解析认证功能可以判断Token是否有效如果Token有效则继续访问后端微服务Token无效或不存在直接拒绝并返回401
# 配置字段
| 名称 | 数据类型 | 填写要求 | 描述 |

View File

@@ -0,0 +1 @@
1.0.0

View File

@@ -13,7 +13,7 @@ import (
func main() {
wrapper.SetCtx(
"jwt-auth", // 配置插件名称
"simple-jwt-auth", // 配置插件名称
wrapper.ParseConfigBy(parseConfig),
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
)

View File

@@ -109,7 +109,6 @@ func (m *RuleMatcher[PluginConfig]) ParseRuleConfig(config gjson.Result,
if keyCount > 0 {
err := parsePluginConfig(config, &pluginConfig)
if err != nil {
proxywasm.LogWarnf("parse global config failed, err:%v", err)
globalConfigError = err
} else {
m.globalConfig = pluginConfig
@@ -185,7 +184,25 @@ func (m RuleMatcher[PluginConfig]) parseHostMatchConfig(config gjson.Result) []H
return hostMatchers
}
func stripPortFromHost(reqHost string) string {
// Port removing code is inspired by
// https://github.com/envoyproxy/envoy/blob/v1.17.0/source/common/http/header_utility.cc#L219
portStart := strings.LastIndexByte(reqHost, ':')
if portStart != -1 {
// According to RFC3986 v6 address is always enclosed in "[]".
// section 3.2.2.
v6EndIndex := strings.LastIndexByte(reqHost, ']')
if v6EndIndex == -1 || v6EndIndex < portStart {
if portStart+1 <= len(reqHost) {
return reqHost[:portStart]
}
}
}
return reqHost
}
func (m RuleMatcher[PluginConfig]) hostMatch(rule RuleConfig[PluginConfig], reqHost string) bool {
reqHost = stripPortFromHost(reqHost)
for _, hostMatch := range rule.hosts {
switch hostMatch.matchType {
case Suffix:

View File

@@ -118,6 +118,19 @@ func TestHostMatch(t *testing.T) {
host: "example.com",
result: false,
},
{
name: "exact port",
config: RuleConfig[customConfig]{
hosts: []HostMatcher{
{
matchType: Exact,
host: "www.example.com",
},
},
},
host: "www.example.com:8080",
result: true,
},
{
name: "any",
config: RuleConfig[customConfig]{

View File

@@ -159,8 +159,7 @@ func (ctx *CommonPluginCtx[PluginConfig]) OnPluginStart(int) types.OnPluginStart
var jsonData gjson.Result
if len(data) == 0 {
if ctx.vm.hasCustomConfig {
ctx.vm.log.Warn("need config")
return types.OnPluginStartStatusFailed
ctx.vm.log.Warn("config is empty, but has ParseConfigFunc")
}
} else {
if !gjson.ValidBytes(data) {
@@ -263,6 +262,10 @@ func (ctx *CommonHttpCtx[PluginConfig]) OnHttpRequestHeaders(numHeaders int, end
return types.ActionContinue
}
ctx.config = config
// To avoid unexpected operations, plugins do not read the binary content body
if IsBinaryRequestBody() {
ctx.needRequestBody = false
}
if ctx.plugin.vm.onHttpRequestHeaders == nil {
return types.ActionContinue
}
@@ -295,6 +298,10 @@ func (ctx *CommonHttpCtx[PluginConfig]) OnHttpResponseHeaders(numHeaders int, en
if ctx.config == nil {
return types.ActionContinue
}
// To avoid unexpected operations, plugins do not read the binary content body
if IsBinaryResponseBody() {
ctx.needResponseBody = false
}
if ctx.plugin.vm.onHttpResponseHeaders == nil {
return types.ActionContinue
}

View File

@@ -14,7 +14,11 @@
package wrapper
import "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
import (
"strings"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
)
func GetRequestScheme() string {
scheme, err := proxywasm.GetHttpRequestHeader(":scheme")
@@ -51,3 +55,29 @@ func GetRequestMethod() string {
}
return method
}
func IsBinaryRequestBody() bool {
contentType, _ := proxywasm.GetHttpRequestHeader("content-type")
if strings.Contains(contentType, "octet-stream") ||
strings.Contains(contentType, "grpc") {
return true
}
encoding, _ := proxywasm.GetHttpRequestHeader("content-encoding")
if encoding != "" {
return true
}
return false
}
func IsBinaryResponseBody() bool {
contentType, _ := proxywasm.GetHttpResponseHeader("content-type")
if strings.Contains(contentType, "octet-stream") ||
strings.Contains(contentType, "grpc") {
return true
}
encoding, _ := proxywasm.GetHttpResponseHeader("content-encoding")
if encoding != "" {
return true
}
return false
}

View File

@@ -23,9 +23,7 @@ import (
"github.com/hudl/fargo"
"istio.io/api/networking/v1alpha3"
versionedclient "istio.io/client-go/pkg/clientset/versioned"
"istio.io/pkg/log"
ctrl "sigs.k8s.io/controller-runtime"
apiv1 "github.com/alibaba/higress/api/networking/v1"
"github.com/alibaba/higress/pkg/common"
@@ -49,7 +47,6 @@ type watcher struct {
cache memory.Cache
mutex *sync.Mutex
stop chan struct{}
istioClient *versionedclient.Clientset
isStop bool
updateCacheWhenEmpty bool
@@ -70,18 +67,6 @@ func NewWatcher(cache memory.Cache, opts ...WatcherOption) (provider.Watcher, er
stop: make(chan struct{}),
}
config, err := ctrl.GetConfig()
if err != nil {
return nil, err
}
ic, err := versionedclient.NewForConfig(config)
if err != nil {
log.Errorf("can not new istio client, err:%v", err)
return nil, err
}
w.istioClient = ic
w.fullRefreshIntervalLimit = DefaultFullRefreshIntervalLimit
for _, opt := range opts {

View File

@@ -29,7 +29,7 @@ metadata:
spec:
containers:
- name: dubbo-demo-provider
image: registry.cn-hangzhou.aliyuncs.com/hinsteny/dubbo-provider-demo:0.0.2
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/dubbo-provider-demo:0.0.3-x86
env:
- name: NACOS_K8S_NAMESPACE
value: higress-conformance-app-backend
@@ -37,3 +37,7 @@ spec:
value: dev
ports:
- containerPort: 20880
resources:
requests:
cpu: 10m
memory: 300Mi

View File

@@ -0,0 +1,208 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// 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 tests
import (
"testing"
"github.com/alibaba/higress/pkg/ingress/kube/configmap"
"github.com/alibaba/higress/test/e2e/conformance/utils/http"
"github.com/alibaba/higress/test/e2e/conformance/utils/kubernetes"
"github.com/alibaba/higress/test/e2e/conformance/utils/suite"
)
func init() {
Register(ConfigmapGzip)
}
var ConfigmapGzip = suite.ConformanceTest{
ShortName: "ConfigmapGzip",
Description: "The Ingress in the higress-conformance-infra namespace uses the configmap gzip.",
Manifests: []string{"tests/configmap-gzip.yaml"},
Features: []suite.SupportedFeature{suite.HTTPConformanceFeature},
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
testcases := []struct {
higressConfig *configmap.HigressConfig
httpAssert http.Assertion
}{
{
higressConfig: &configmap.HigressConfig{
Gzip: &configmap.Gzip{
Enable: false,
MinContentLength: 1024,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
},
httpAssert: http.Assertion{
Meta: http.AssertionMeta{
TestCaseName: "case1: disable gzip output",
TargetBackend: "web-backend",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "foo.com",
Path: "/foo",
Method: "GET",
Headers: map[string]string{
"Accept-Encoding": "*",
},
},
},
Response: http.AssertionResponse{
ExpectedResponseNoRequest: true,
ExpectedResponse: http.Response{
StatusCode: 200,
AbsentHeaders: []string{"content-encoding"},
},
},
},
},
{
higressConfig: &configmap.HigressConfig{
Gzip: &configmap.Gzip{
Enable: true,
MinContentLength: 100,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
},
httpAssert: http.Assertion{
Meta: http.AssertionMeta{
TestCaseName: "case2: enable gzip output",
TargetBackend: "web-backend",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "foo.com",
Path: "/foo",
Method: "GET",
Headers: map[string]string{
"Accept-Encoding": "*",
},
},
},
Response: http.AssertionResponse{
ExpectedResponseNoRequest: true,
ExpectedResponse: http.Response{
StatusCode: 200,
},
AdditionalResponseHeaders: map[string]string{"content-encoding": "gzip"},
},
},
},
{
higressConfig: &configmap.HigressConfig{
Gzip: &configmap.Gzip{
Enable: true,
MinContentLength: 4096,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/json", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
},
httpAssert: http.Assertion{
Meta: http.AssertionMeta{
TestCaseName: "case3: disable gzip output because content length less hhan 4096 ",
TargetBackend: "web-backend",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "foo.com",
Path: "/foo",
Method: "GET",
Headers: map[string]string{
"Accept-Encoding": "*",
},
},
},
Response: http.AssertionResponse{
ExpectedResponseNoRequest: true,
ExpectedResponse: http.Response{
StatusCode: 200,
AbsentHeaders: []string{"content-encoding"},
},
},
},
},
{
higressConfig: &configmap.HigressConfig{
Gzip: &configmap.Gzip{
Enable: true,
MinContentLength: 100,
ContentType: []string{"text/html", "text/css", "text/plain", "text/xml", "application/javascript", "application/xhtml+xml", "image/svg+xml"},
DisableOnEtagHeader: true,
MemoryLevel: 5,
WindowBits: 12,
ChunkSize: 4096,
CompressionLevel: "BEST_COMPRESSION",
CompressionStrategy: "DEFAULT_STRATEGY",
},
},
httpAssert: http.Assertion{
Meta: http.AssertionMeta{
TestCaseName: "case4: disable gzip output because application/json missed in content types ",
TargetBackend: "web-backend",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "foo.com",
Path: "/foo",
Method: "GET",
Headers: map[string]string{
"Accept-Encoding": "*",
},
},
},
Response: http.AssertionResponse{
ExpectedResponseNoRequest: true,
ExpectedResponse: http.Response{
StatusCode: 200,
AbsentHeaders: []string{"content-encoding"},
},
},
},
},
}
t.Run("Configmap Gzip", func(t *testing.T) {
for _, testcase := range testcases {
err := kubernetes.ApplyConfigmapDataWithYaml(suite.Client, "higress-system", "higress-config", "higress", testcase.higressConfig)
if err != nil {
t.Fatalf("can't apply conifgmap %s in namespace %s for data key %s", "higress-config", "higress-system", "higress")
}
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase.httpAssert)
}
})
},
}

View File

@@ -0,0 +1,32 @@
# Copyright (c) 2022 Alibaba Group Holding Ltd.
#
# 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.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: higress-conformance-infra-configmap-gzip-test
namespace: higress-conformance-infra
spec:
ingressClassName: higress
rules:
- host: "foo.com"
http:
paths:
- pathType: Prefix
path: "/foo"
backend:
service:
name: infra-backend-v3
port:
number: 8080

View File

@@ -0,0 +1,143 @@
// Copyright (c) 2023 Alibaba Group Holding Ltd.
//
// 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 tests
import (
"testing"
"github.com/alibaba/higress/test/e2e/conformance/utils/http"
"github.com/alibaba/higress/test/e2e/conformance/utils/suite"
)
func init() {
Register(WasmPluginsKeyAuth)
}
var WasmPluginsKeyAuth = suite.ConformanceTest{
ShortName: "WasmPluginsKeyAuth",
Description: "The Ingress in the higress-conformance-infra namespace test the key-auth WASM plugin.",
Manifests: []string{"tests/go-wasm-key-auth.yaml"},
Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature},
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
testcases := []http.Assertion{
{
Meta: http.AssertionMeta{
TestCaseName: "case 1: Successful authentication",
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "foo.com",
Path: "/foo",
Headers: map[string]string{"x-api-key": "token11111111111111111111"},
},
ExpectedRequest: &http.ExpectedRequest{
Request: http.Request{
Host: "foo.com",
Path: "/foo",
Headers: map[string]string{"X-Mse-Consumer": "consumer1"},
},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 200,
},
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 2: No Key Authentication information found",
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "foo.com",
Path: "/foo",
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 401,
},
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 3: Invalid token",
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "foo.com",
Path: "/foo",
Headers: map[string]string{"x-api-key": "xxxxxxxxxnotfoundtoken"},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 403,
},
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 4: Unauthorized consumer",
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "foo.com",
Path: "/foo",
Headers: map[string]string{"x-api-key": "token22222222222222222222"},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 403,
},
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 5: Muti Key Authentication information found",
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "foo.com",
Path: "/foo",
Headers: map[string]string{"apikey": "token11111111111111111111", "x-api-key": "token11111111111111111111"},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 401,
},
},
},
}
t.Run("WasmPlugins key-auth", func(t *testing.T) {
for _, testcase := range testcases {
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase)
}
})
},
}

View File

@@ -0,0 +1,60 @@
# Copyright (c) 2022 Alibaba Group Holding Ltd.
#
# 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.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
name: wasmplugin-key-auth
namespace: higress-conformance-infra
spec:
ingressClassName: higress
rules:
- host: "foo.com"
http:
paths:
- pathType: Prefix
path: "/foo"
backend:
service:
name: infra-backend-v1
port:
number: 8080
---
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
name: key-auth
namespace: higress-system
spec:
defaultConfig:
consumers:
- credential: token11111111111111111111
name: consumer1
- credential: token22222222222222222222
name: consumer2
global_auth: false
keys:
- x-api-key
- apikey
in_header: true
defaultConfigDisable: false
matchRules:
- config:
allow:
- consumer1
configDisable: false
ingress:
- higress-conformance-infra/wasmplugin-key-auth
url: file:///opt/plugins/wasm-go/extensions/key-auth/plugin.wasm

View File

@@ -27,8 +27,8 @@ func init() {
var WasmPluginsJwtAuth = suite.ConformanceTest{
ShortName: "WasmPluginsJwtAuth",
Description: "The Ingress in the higress-conformance-infra namespace test the jwt-auth wasmplugins.",
Manifests: []string{"tests/go-wasm-jwt-auth.yaml"},
Description: "The Ingress in the higress-conformance-infra namespace test the simple-jwt-auth wasmplugins.",
Manifests: []string{"tests/go-wasm-simple-jwt-auth.yaml"},
Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature},
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
testcases := []http.Assertion{
@@ -51,7 +51,7 @@ var WasmPluginsJwtAuth = suite.ConformanceTest{
},
},
}
t.Run("WasmPlugins jwt-auth", func(t *testing.T) {
t.Run("WasmPlugins simple-jwt-auth", func(t *testing.T) {
for _, testcase := range testcases {
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase)
}

View File

@@ -40,4 +40,4 @@ spec:
defaultConfig:
token_headers: token
token_secret_key: Dav7kfq3iA8S!JUj8&CUkdnQe72E@Cw6
url: file:///opt/plugins/wasm-go/extensions/jwt-auth/plugin.wasm
url: file:///opt/plugins/wasm-go/extensions/simple-jwt-auth/plugin.wasm

View File

@@ -34,6 +34,7 @@ var HTTPRouteCanaryHeader = suite.ConformanceTest{
testcases := []http.Assertion{
{
Meta: http.AssertionMeta{
TestCaseName: "case 1: canary header value matches",
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
@@ -49,8 +50,10 @@ var HTTPRouteCanaryHeader = suite.ConformanceTest{
StatusCode: 200,
},
},
}, {
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 2: canary header value does not match",
TargetBackend: "infra-backend-v2",
TargetNamespace: "higress-conformance-infra",
},
@@ -65,8 +68,10 @@ var HTTPRouteCanaryHeader = suite.ConformanceTest{
StatusCode: 200,
},
},
}, {
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 3: canary header value matches when the exact path matches",
TargetBackend: "infra-backend-v2",
TargetNamespace: "higress-conformance-infra",
},
@@ -82,8 +87,10 @@ var HTTPRouteCanaryHeader = suite.ConformanceTest{
StatusCode: 200,
},
},
}, {
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 4: canary header value matches when the prefix path matches",
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
@@ -102,6 +109,7 @@ var HTTPRouteCanaryHeader = suite.ConformanceTest{
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 5: canary header value does not match when the exact path matches",
TargetBackend: "infra-backend-v3",
TargetNamespace: "higress-conformance-infra",
},
@@ -119,6 +127,7 @@ var HTTPRouteCanaryHeader = suite.ConformanceTest{
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 6: canary header value does not match when the prefix path matches",
TargetBackend: "infra-backend-v3",
TargetNamespace: "higress-conformance-infra",
},
@@ -134,6 +143,87 @@ var HTTPRouteCanaryHeader = suite.ConformanceTest{
},
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 7: canary header pattern matches",
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Path: "/baz",
Host: "canary.higress.io",
Headers: map[string]string{
"traffic-split-higress": "test.com",
},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 200,
},
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 8: canary header pattern matches including the suffix",
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Path: "/baz",
Host: "canary.higress.io",
Headers: map[string]string{
"traffic-split-higress": "test.com.abc",
},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 200,
},
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 9: canary header is not set",
TargetBackend: "infra-backend-v2",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Path: "/baz",
Host: "canary.higress.io",
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 200,
},
},
},
{
Meta: http.AssertionMeta{
TestCaseName: "case 10: canary header pattern does not match",
TargetBackend: "infra-backend-v2",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Path: "/baz",
Host: "canary.higress.io",
Headers: map[string]string{
"traffic-split-higress": "test.org",
},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 200,
},
},
},
}
t.Run("Canary HTTPRoute Traffic Split", func(t *testing.T) {

Some files were not shown because too many files have changed in this diff Show More