Compare commits

..

72 Commits

Author SHA1 Message Date
澄潭
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
澄潭
26654aefc0 add the catch all case (#623) 2023-11-03 20:19:43 +08:00
澄潭
70176cde3e support sort httproute when use gateway api (#622) 2023-11-03 17:23:43 +08:00
Jun
7b1f538d38 Add detecting higress installed by helm or not before install (#620) 2023-11-02 15:41:01 +08:00
澄潭
344035698a Update Makefile.core.mk 2023-11-02 14:07:26 +08:00
Jun
9136908354 feat:add installation for higress standalone in local docker environment (#606)
Co-authored-by: 澄潭 <zty98751@alibaba-inc.com>
2023-11-02 14:02:23 +08:00
澄潭
de1dd3bfbc rel: Release version 1.3.0 (#619) 2023-11-02 11:49:00 +08:00
澄潭
0c1db17de6 adjust go mod (#618) 2023-11-02 11:46:19 +08:00
澄潭
8cbe16f77c Update build-and-test.yaml 2023-11-02 11:00:04 +08:00
澄潭
8ed4c5609a Update build-image-and-push.yaml 2023-11-02 10:31:22 +08:00
澄潭
9ea1903ce6 Update build-and-test.yaml 2023-11-02 10:08:29 +08:00
澄潭
6835486725 use higress-group nottinygc (#615) 2023-11-01 16:49:01 +08:00
澄潭
265df42456 support v1beta1 gateway-api (#614) 2023-11-01 09:32:46 +08:00
Ffyyt
3b1b621627 feat:add oidc wasm plugin (#568) 2023-10-31 17:15:55 +08:00
WeixinX
901ad9619d Record the progress of the OSPP 2023 hgctl project (#453) 2023-10-27 17:36:49 +08:00
Kent Dong
4a5127fedc fix: Fix local image building targets (#599) 2023-10-25 16:59:34 +08:00
澄潭
4e44e7a1bb fix hmac auth (#603) 2023-10-25 11:54:21 +08:00
Shiqi Wang
b54a2e7387 fix fallback envoy version check bug (#441)
Co-authored-by: shiqi wang <shiqi.wang1@huawei.com>
Co-authored-by: 澄潭 <zty98751@alibaba-inc.com>
2023-10-24 14:51:40 +08:00
澄潭
124caf8785 Update Makefile.core.mk 2023-10-24 13:42:48 +08:00
澄潭
754ec71d6e cds use fallback cluster (#600) 2023-10-24 11:44:52 +08:00
澄潭
d8e91851d9 add gc-test plugin (#597) 2023-10-23 15:21:01 +08:00
澄潭
86b223bc75 replace with higress-group/nottinygc (#588) 2023-10-19 20:31:23 +08:00
Tom Kerkhove
a18879bf86 security: Remove GitHub PAT from code samples (#584) 2023-10-17 18:56:19 +08:00
Kent Dong
970cfd44ee fix: No update prompt if the target folder contains an unconfigured Higress instance (#583) 2023-10-16 10:59:13 +08:00
澄潭
f685b0353a add istio&envoy image tag (#578) 2023-10-10 09:34:34 +08:00
澄潭
e135789c3e add new envoy tar (#576) 2023-10-09 11:36:12 +08:00
澄潭
3e72d4b1f0 Suppport fallback cluster (#575) 2023-10-09 09:37:16 +08:00
WeixinX
1ded5322a5 feat: Add request and response transformer wasm plugin (#549) 2023-10-07 15:47:20 +08:00
澄潭
be8563765e support custom group in httproute (#561) 2023-09-27 09:37:38 +08:00
Jun
45c4c80a66 fix namespace missing in helm chart (#531) 2023-09-27 09:31:24 +08:00
澄潭
d8c34bb863 fix the delete service issue in nacos registry (#553) 2023-09-27 09:30:39 +08:00
Jun
4e392d1cf6 update higress helm chart version to latest in profiles for installation (#560) 2023-09-27 09:30:24 +08:00
Jun
5b663ae412 feat: add hgctl dashboard controller subcommand to open higress controller debug web ui (#564)
Co-authored-by: Xunzhuo <bitliu@tencent.com>
2023-09-26 20:27:57 +08:00
Kent Dong
fcf19535f9 fix: Fix incorrect logging functions used in eureka registry integration (#563) 2023-09-25 22:05:58 +08:00
rinfx
14e43aa921 update source for images used by ci (#557)
Co-authored-by: Xunzhuo <bitliu@tencent.com>
2023-09-25 17:09:11 +08:00
yingjianjian
64ccbab29c feat: add request block regular matching and modify block_ Urls are e… (#517) 2023-09-25 16:48:38 +08:00
澄潭
945787f7dc fix reconcile of mcpbridge (#559) 2023-09-25 10:42:19 +08:00
266 changed files with 44518 additions and 958 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

@@ -38,10 +38,9 @@ jobs:
path: |-
envoy
istio
external
.git/modules
key: ${{ runner.os }}-submodules-${{ github.run_id }}
restore-keys: ${{ runner.os }}-submodules
key: ${{ runner.os }}-submodules-new-${{ github.run_id }}
restore-keys: ${{ runner.os }}-submodules-new
- run: git stash # restore patch
@@ -85,10 +84,9 @@ jobs:
path: |-
envoy
istio
external
.git/modules
key: ${{ runner.os }}-submodules-${{ github.run_id }}
restore-keys: ${{ runner.os }}-submodules
key: ${{ runner.os }}-submodules-new-${{ github.run_id }}
restore-keys: ${{ runner.os }}-submodules-new
- run: git stash # restore patch
@@ -134,59 +132,17 @@ jobs:
path: |-
envoy
istio
external
.git/modules
key: ${{ runner.os }}-submodules-${{ github.run_id }}
restore-keys: ${{ runner.os }}-submodules
key: ${{ runner.os }}-submodules-new-${{ github.run_id }}
restore-keys: ${{ runner.os }}-submodules-new
- run: git stash # restore patch
- 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
external
.git/modules
key: ${{ runner.os }}-submodules-${{ github.run_id }}
restore-keys: ${{ runner.os }}-submodules
- 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

@@ -41,7 +41,7 @@ jobs:
envoy
istio
.git/modules
key: ${{ runner.os }}-submodules-${{ github.run_id }}
key: ${{ runner.os }}-submodules-new-${{ github.run_id }}
restore-keys: ${{ runner.os }}-submodules-new
- name: Calculate Docker metadata

View File

@@ -27,6 +27,7 @@ header:
- 'tools/'
- 'test/README.md'
- 'pkg/cmd/hgctl/testdata/config'
- 'pkg/cmd/hgctl/manifests'
comment: on-failure
dependency:

View File

@@ -79,15 +79,15 @@ $(ARM64_OUT_LINUX)/higress:
GOPROXY=$(GOPROXY) GOOS=linux GOARCH=arm64 LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh ./out/linux_arm64/ $(HIGRESS_BINARIES)
.PHONY: build-hgctl
build-hgctl: $(OUT)
build-hgctl: prebuild $(OUT)
GOPROXY=$(GOPROXY) GOOS=$(GOOS_LOCAL) GOARCH=$(GOARCH_LOCAL) LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh $(OUT)/ $(HGCTL_BINARIES)
.PHONY: build-linux-hgctl
build-linux-hgctl: $(OUT)
build-linux-hgctl: prebuild $(OUT)
GOPROXY=$(GOPROXY) GOOS=linux GOARCH=$(GOARCH_LOCAL) LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh $(OUT_LINUX)/ $(HGCTL_BINARIES)
.PHONY: build-hgctl-multiarch
build-hgctl-multiarch: $(OUT)
build-hgctl-multiarch: prebuild $(OUT)
GOPROXY=$(GOPROXY) GOOS=linux GOARCH=amd64 LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh ./out/linux_amd64/ $(HGCTL_BINARIES)
GOPROXY=$(GOPROXY) GOOS=linux GOARCH=arm64 LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh ./out/linux_arm64/ $(HGCTL_BINARIES)
GOPROXY=$(GOPROXY) GOOS=darwin GOARCH=amd64 LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh ./out/darwin_amd64/ $(HGCTL_BINARIES)
@@ -137,28 +137,30 @@ 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.0.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.0.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
cd external/istio; rm -rf out/linux_arm64; GOOS_LOCAL=linux TARGET_OS=linux TARGET_ARCH=arm64 BUILD_WITH_CONTAINER=1 make build-linux
build-pilot-local:
cd external/istio; rm -rf out/linux_${GOARCH_LOCAL}; GOOS_LOCAL=linux TARGET_OS=linux TARGET_ARCH=${GOARCH_LOCAL} BUILD_WITH_CONTAINER=1 make build-linux
build-gateway: prebuild external/package/envoy-amd64.tar.gz external/package/envoy-arm64.tar.gz build-pilot
cd external/istio; BUILD_WITH_CONTAINER=1 BUILDX_PLATFORM=true DOCKER_BUILD_VARIANTS=default DOCKER_TARGETS="docker.proxyv2" make docker
build-gateway-local: prebuild external/package/envoy-amd64.tar.gz external/package/envoy-arm64.tar.gz build-pilot
cd external/istio; BUILD_WITH_CONTAINER=1 BUILDX_PLATFORM=false DOCKER_BUILD_VARIANTS=default DOCKER_TARGETS="docker.proxyv2" make docker
cd external/istio; rm -rf out/linux_${GOARCH_LOCAL}; GOOS_LOCAL=linux TARGET_OS=linux BUILD_WITH_CONTAINER=1 BUILDX_PLATFORM=false DOCKER_BUILD_VARIANTS=default DOCKER_TARGETS="docker.proxyv2" make docker
build-istio: prebuild build-pilot
cd external/istio; BUILD_WITH_CONTAINER=1 BUILDX_PLATFORM=true DOCKER_BUILD_VARIANTS=default DOCKER_TARGETS="docker.pilot" make docker
build-istio-local: prebuild build-pilot
cd external/istio; BUILD_WITH_CONTAINER=1 BUILDX_PLATFORM=false DOCKER_BUILD_VARIANTS=default DOCKER_TARGETS="docker.pilot" make docker
build-istio-local: prebuild build-pilot-local
cd external/istio; rm -rf out/linux_${GOARCH_LOCAL}; GOOS_LOCAL=linux TARGET_OS=linux BUILD_WITH_CONTAINER=1 BUILDX_PLATFORM=false DOCKER_BUILD_VARIANTS=default DOCKER_TARGETS="docker.pilot" make docker
build-wasmplugins:
./tools/hack/build-wasm-plugins.sh
@@ -174,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 ?= 1.2.0
ISTIO_LATEST_IMAGE_TAG ?= 1.2.0
ENVOY_LATEST_IMAGE_TAG ?= sha-34054f8
ISTIO_LATEST_IMAGE_TAG ?= sha-34054f8
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'
@@ -255,16 +257,16 @@ 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 registry.cn-hangzhou.aliyuncs.com/hinsteny/dubbo-provider-demo 0.0.1
tools/hack/docker-pull-image.sh registry.cn-hangzhou.aliyuncs.com/hinsteny/nacos-standlone-rc3 1.0.0-RC3
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 registry.cn-hangzhou.aliyuncs.com/2456868764/httpbin 1.0.2
tools/hack/kind-load-image.sh registry.cn-hangzhou.aliyuncs.com/hinsteny/dubbo-provider-demo 0.0.1
tools/hack/kind-load-image.sh registry.cn-hangzhou.aliyuncs.com/hinsteny/nacos-standlone-rc3 1.0.0-RC3
tools/hack/docker-pull-image.sh docker.io/alihigress/httpbin 1.0.2
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 registry.cn-hangzhou.aliyuncs.com/2456868764/httpbin 1.0.2
tools/hack/kind-load-image.sh docker.io/alihigress/httpbin 1.0.2
tools/hack/kind-load-image.sh docker.io/charlie1380/eureka-registry-provider v0.3.0
tools/hack/kind-load-image.sh docker.io/bitinit/eureka latest
# run-higress-e2e-test starts to run ingress e2e tests.

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.2.0
v1.3.2

View File

@@ -0,0 +1,111 @@
diff -Naur envoy/contrib/custom_cluster_plugins/cluster_fallback/source/filter.cc envoy-new/contrib/custom_cluster_plugins/cluster_fallback/source/filter.cc
--- envoy/contrib/custom_cluster_plugins/cluster_fallback/source/filter.cc 2023-10-08 15:01:21.960871500 +0800
+++ envoy-new/contrib/custom_cluster_plugins/cluster_fallback/source/filter.cc 2023-09-27 17:03:41.613256338 +0800
@@ -60,7 +60,7 @@
for (const auto& cluster_name : first_item->second) {
if (hasHealthHost(cluster_name)) {
- return base.clone(cluster_name);
+ return base.clone(cluster_name, first_item->first);
}
}
@@ -75,7 +75,8 @@
auto search = clusters_config_.find(route_entry.clusterName());
if (search == clusters_config_.end()) {
- ENVOY_LOG(warn, "there is no fallback cluster config, the original routing cluster is returned");
+ ENVOY_LOG(warn,
+ "there is no fallback cluster config, the original routing cluster is returned");
return cluster_entry.getRouteConstSharedPtr();
}
@@ -87,7 +88,7 @@
for (const auto& cluster_name : search->second) {
if (hasHealthHost(cluster_name)) {
- return cluster_entry.clone(cluster_name);
+ return cluster_entry.clone(cluster_name, search->first);
}
}
diff -Naur envoy/source/common/http/headers.h envoy-new/source/common/http/headers.h
--- envoy/source/common/http/headers.h 2023-10-08 15:01:21.968871828 +0800
+++ envoy-new/source/common/http/headers.h 2023-09-27 18:48:50.059419606 +0800
@@ -124,6 +124,7 @@
const LowerCaseString TriStartTime{"req-start-time"};
const LowerCaseString TriRespStartTime{"resp-start-time"};
const LowerCaseString EnvoyOriginalHost{"original-host"};
+ const LowerCaseString HigressOriginalService{"x-higress-original-service"};
} AliExtendedValues;
#endif
};
diff -Naur envoy/source/common/router/config_impl.cc envoy-new/source/common/router/config_impl.cc
--- envoy/source/common/router/config_impl.cc 2023-10-08 15:01:21.968871828 +0800
+++ envoy-new/source/common/router/config_impl.cc 2023-09-27 18:49:18.656592237 +0800
@@ -563,7 +563,6 @@
route.name());
}
// End Added
-
}
bool RouteEntryImplBase::evaluateRuntimeMatch(const uint64_t random_value) const {
@@ -662,6 +661,10 @@
}
#if defined(ALIMESH)
+ if (!origin_cluster_name_.empty()) {
+ headers.addCopy(Http::CustomHeaders::get().AliExtendedValues.HigressOriginalService,
+ origin_cluster_name_);
+ }
headers.setReferenceKey(Http::CustomHeaders::get().AliExtendedValues.EnvoyOriginalHost,
headers.getHostValue());
#endif
diff -Naur envoy/source/common/router/config_impl.h envoy-new/source/common/router/config_impl.h
--- envoy/source/common/router/config_impl.h 2023-10-08 15:01:21.968871828 +0800
+++ envoy-new/source/common/router/config_impl.h 2023-09-27 18:59:11.196893507 +0800
@@ -584,9 +584,13 @@
return internal_active_redirect_policy_;
}
- RouteConstSharedPtr clone(const std::string& name) const {
- return std::make_shared<DynamicRouteEntry>(this, name);
+ RouteConstSharedPtr clone(const std::string& name, const std::string& origin_cluster = "") const {
+ auto entry = std::make_shared<DynamicRouteEntry>(this, name);
+ entry->setOriginClusterName(origin_cluster);
+ return entry;
}
+
+ void setOriginClusterName(const std::string& name) const { origin_cluster_name_ = name; }
#endif
uint32_t retryShadowBufferLimit() const override { return retry_shadow_buffer_limit_; }
const std::vector<ShadowPolicyPtr>& shadowPolicies() const override { return shadow_policies_; }
@@ -787,11 +791,17 @@
return parent_->internalActiveRedirectPolicy();
}
- RouteConstSharedPtr clone(const std::string& name) const {
- return std::make_shared<Envoy::Router::RouteEntryImplBase::DynamicRouteEntry>(parent_, name);
+ RouteConstSharedPtr clone(const std::string& name,
+ const std::string& origin_cluster = "") const {
+ auto entry =
+ std::make_shared<Envoy::Router::RouteEntryImplBase::DynamicRouteEntry>(parent_, name);
+ entry->setOriginClusterName(origin_cluster);
+ return entry;
}
virtual RouteConstSharedPtr getRouteConstSharedPtr() const { return shared_from_this(); }
+
+ void setOriginClusterName(const std::string& name) { parent_->setOriginClusterName(name); }
#endif
private:
@@ -1039,6 +1049,7 @@
#if defined(ALIMESH)
const InternalActiveRedirectPoliciesImpl internal_active_redirect_policy_;
+ mutable std::string origin_cluster_name_;
#endif
};

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

182
go.mod
View File

@@ -10,17 +10,22 @@ replace github.com/chzyer/logex => github.com/chzyer/logex v1.1.11-0.20170329064
// Avoid pulling in incompatible libraries
replace github.com/docker/distribution => github.com/docker/distribution v0.0.0-20191216044856-a8371794149d
replace github.com/docker/docker => github.com/moby/moby v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible
// Client-go does not handle different versions of mergo due to some breaking changes - use the matching version
replace github.com/imdario/mergo => github.com/imdario/mergo v0.3.5
require (
github.com/AlecAivazis/survey/v2 v2.3.6
github.com/agiledragon/gomonkey/v2 v2.9.0
github.com/avast/retry-go/v4 v4.3.4
github.com/compose-spec/compose-go v1.8.2
github.com/docker/cli v20.10.20+incompatible
github.com/docker/compose/v2 v2.0.0-00010101000000-000000000000
github.com/docker/docker v20.10.20+incompatible
github.com/dubbogo/go-zookeeper v1.0.4-0.20211212162352-f9d2183d89d5
github.com/dubbogo/gost v1.13.1
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1
github.com/fatih/color v1.14.1
github.com/fatih/structtag v1.2.0
github.com/gogo/protobuf v1.3.2
github.com/golang/protobuf v1.5.2
github.com/google/go-cmp v0.5.9
@@ -28,25 +33,31 @@ require (
github.com/hashicorp/consul/api v1.23.0
github.com/hashicorp/go-multierror v1.1.1
github.com/hudl/fargo v1.4.0
github.com/iancoleman/orderedmap v0.3.0
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/mapstructure v1.5.0
github.com/nacos-group/nacos-sdk-go v1.0.8
github.com/nacos-group/nacos-sdk-go/v2 v2.1.2
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.2.1
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
github.com/spf13/cobra v1.6.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.8.1
github.com/stretchr/testify v1.8.3
go.uber.org/atomic v1.9.0
google.golang.org/grpc v1.48.0
google.golang.org/protobuf v1.28.0
google.golang.org/protobuf v1.28.1
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
istio.io/api v0.0.0-20211122181927-8da52c66ff23
istio.io/client-go v1.12.0-rc.1.0.20211118171212-b744b6f111e4
istio.io/gogo-genproto v0.0.0-20211115195057-0e34bdd2be67
istio.io/istio v0.0.0
istio.io/pkg v0.0.0-20211115195056-e379f31ee62a
k8s.io/api v0.22.5
k8s.io/apimachinery v0.22.5
k8s.io/api v0.24.1
k8s.io/apimachinery v0.24.1
k8s.io/cli-runtime v0.22.2
k8s.io/client-go v0.22.5
k8s.io/client-go v0.24.1
k8s.io/kubectl v0.22.2
sigs.k8s.io/controller-runtime v0.10.2
sigs.k8s.io/yaml v1.3.0
@@ -69,56 +80,63 @@ require (
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.2 // indirect
github.com/Masterminds/squirrel v1.5.0 // indirect
github.com/Microsoft/go-winio v0.5.0 // indirect
github.com/Microsoft/hcsshim v0.8.21 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/Microsoft/hcsshim v0.9.6 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/RageCage64/multilinediff v0.2.0 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1704 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
github.com/aws/aws-sdk-go v1.41.7 // indirect
github.com/aws/aws-sdk-go v1.43.16 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect
github.com/braydonk/yaml v0.7.0 // indirect
github.com/buger/goterm v1.0.4 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cenkalti/backoff/v4 v4.1.1 // indirect
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect
github.com/clbanning/mxj v1.8.4 // indirect
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
github.com/cncf/xds/go v0.0.0-20220520190051-1e77728a1eaa // indirect
github.com/containerd/containerd v1.5.7 // indirect
github.com/containerd/continuity v0.1.0 // indirect
github.com/compose-spec/godotenv v1.1.1 // indirect
github.com/containerd/cgroups v1.0.4 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/containerd/containerd v1.6.14 // indirect
github.com/containerd/continuity v0.3.0 // indirect
github.com/containerd/typeurl v1.0.2 // indirect
github.com/coreos/go-oidc/v3 v3.1.0 // indirect
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0 // indirect
github.com/docker/cli v20.10.7+incompatible // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v20.10.7+incompatible // indirect
github.com/docker/docker-credential-helpers v0.6.3 // indirect
github.com/distribution/distribution/v3 v3.0.0-20221201083218-92d136e113cf // indirect
github.com/docker/buildx v0.9.1 // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.7.0 // indirect
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/envoyproxy/protoc-gen-validate v0.1.0 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
github.com/fatih/color v1.14.1 // indirect
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/fvbommel/sortorder v1.0.1 // indirect
github.com/fvbommel/sortorder v1.0.2 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-kit/log v0.1.0 // indirect
github.com/go-logfmt/logfmt v0.5.0 // indirect
github.com/go-logr/logr v1.2.2 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.5 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.4.8 // indirect
github.com/gofrs/flock v0.8.0 // indirect
github.com/gogo/googleapis v1.4.1 // indirect
github.com/golang-jwt/jwt/v4 v4.2.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/mock v1.6.0 // indirect
@@ -133,23 +151,29 @@ require (
github.com/gosuri/uitable v0.0.4 // indirect
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-hclog v1.5.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-version v1.3.0 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/serf v0.10.1 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jaguilar/vt100 v0.0.0-20150826170717-2703a27b14ea // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jmoiron/sqlx v1.3.1 // indirect
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.13.6 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.15.9 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/lestrrat-go/backoff/v2 v2.0.7 // indirect
@@ -162,20 +186,27 @@ require (
github.com/lestrrat/go-strftime v0.0.0-20180220042222-ba3bf9c1d042 // indirect
github.com/lib/pq v1.10.0 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.12 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mattn/go-shellwords v1.0.12 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/miekg/dns v1.1.43 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/buildkit v0.10.4 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/spdystream v0.2.0 // indirect
github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 // indirect
github.com/moby/sys/mount v0.3.0 // indirect
github.com/moby/sys/mountinfo v0.6.0 // indirect
github.com/moby/sys/signal v0.7.0 // indirect
github.com/moby/sys/symlink v0.2.0 // indirect
github.com/moby/term v0.0.0-20221128092401-c43b287e0e0f // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
@@ -183,9 +214,11 @@ require (
github.com/natefinch/lumberjack v2.0.0+incompatible // indirect
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/opencontainers/runc v1.0.2 // indirect
github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
github.com/opencontainers/runc v1.1.3 // indirect
github.com/openshift/api v0.0.0-20200713203337-b2494ecb17dd // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.12.2 // indirect
@@ -194,12 +227,19 @@ require (
github.com/prometheus/procfs v0.7.3 // indirect
github.com/prometheus/statsd_exporter v0.21.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.6.1 // indirect
github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc // indirect
github.com/russross/blackfriday v1.5.2 // indirect
github.com/russross/blackfriday v1.6.0 // indirect
github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/theupdateframework/notary v0.7.0 // indirect
github.com/tonistiigi/fsutil v0.0.0-20220930225714-4638ad635be5 // indirect
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
github.com/toolkits/concurrent v0.0.0-20150624120057-a4371d70e3e3 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
@@ -207,14 +247,14 @@ require (
github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect
github.com/yl2chen/cidranger v1.0.2 // indirect
go.opencensus.io v0.23.0 // indirect
go.opentelemetry.io/proto/otlp v0.7.0 // indirect
go.opentelemetry.io/proto/otlp v0.12.0 // indirect
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
go.uber.org/multierr v1.7.0 // indirect
go.uber.org/zap v1.21.0 // indirect
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
golang.org/x/net v0.12.0 // indirect
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
golang.org/x/oauth2 v0.6.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/term v0.10.0 // indirect
@@ -225,7 +265,7 @@ require (
gomodules.xyz/orderedmap v0.1.0 // indirect
google.golang.org/api v0.61.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12 // indirect
google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 // indirect
gopkg.in/gcfg.v1 v1.2.3 // indirect
gopkg.in/gorp.v1 v1.7.2 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
@@ -233,10 +273,9 @@ require (
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiserver v0.22.2 // indirect
k8s.io/component-base v0.22.2 // indirect
k8s.io/klog/v2 v2.40.1 // indirect
k8s.io/apiserver v0.22.5 // indirect
k8s.io/component-base v0.22.5 // indirect
k8s.io/klog/v2 v2.60.1 // indirect
k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c // indirect
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect
oras.land/oras-go v0.4.0 // indirect
@@ -259,23 +298,6 @@ replace istio.io/client-go => ./external/client-go
replace istio.io/istio => ./external/istio
replace (
github.com/go-logr/logr => github.com/go-logr/logr v0.4.0
k8s.io/api => k8s.io/api v0.22.2
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.22.2
k8s.io/apimachinery => k8s.io/apimachinery v0.22.2
k8s.io/cli-runtime => k8s.io/cli-runtime v0.22.2
k8s.io/client-go => k8s.io/client-go v0.22.2
k8s.io/component-base => k8s.io/component-base v0.22.2
k8s.io/klog/v2 => k8s.io/klog/v2 v2.10.0
k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20211020163157-7327e2aaee2b // indirect
k8s.io/kubectl => k8s.io/kubectl v0.22.2
k8s.io/utils => k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect
sigs.k8s.io/gateway-api => sigs.k8s.io/gateway-api v0.4.0 // indirect
sigs.k8s.io/kustomize/api => sigs.k8s.io/kustomize/api v0.8.11 // indirect
sigs.k8s.io/kustomize/kyaml => sigs.k8s.io/kustomize/kyaml v0.11.0 // indirect
)
require (
github.com/evanphx/json-patch/v5 v5.6.0
github.com/google/yamlfmt v0.10.0
@@ -285,3 +307,49 @@ require (
knative.dev/networking v0.0.0-20220302134042-e8b2eb995165
knative.dev/pkg v0.0.0-20220301181942-2fdd5f232e77
)
replace (
github.com/Sirupsen/logrus => github.com/sirupsen/logrus v1.9.3
github.com/go-logr/logr => github.com/go-logr/logr v0.4.0
k8s.io/api => k8s.io/api v0.22.2
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.22.2
k8s.io/apimachinery => k8s.io/apimachinery v0.22.2
k8s.io/cli-runtime => k8s.io/cli-runtime v0.22.2
k8s.io/client-go => k8s.io/client-go v0.22.2
k8s.io/code-generator => k8s.io/code-generator v0.22.2
k8s.io/component-base => k8s.io/component-base v0.22.2
k8s.io/component-helpers => k8s.io/component-helpers v0.22.2
k8s.io/klog/v2 => k8s.io/klog/v2 v2.10.0
k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e
k8s.io/kubectl => k8s.io/kubectl v0.22.2
k8s.io/metrics => k8s.io/metrics v0.22.2
k8s.io/utils => k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect
sigs.k8s.io/kustomize/api => sigs.k8s.io/kustomize/api v0.8.11 // indirect
sigs.k8s.io/kustomize/kyaml => sigs.k8s.io/kustomize/kyaml v0.11.0 // indirect
)
// for pkg/cmd/hgctl/docker/compose.go
// TODO(WeixinX): Wait for the dependency library to upgrade, such as github.com/go-logr/logr from v0.4.0 to v1.2+
// replace (
// github.com/compose-spec/compose-go => github.com/compose-spec/compose-go v1.8.2
// github.com/cucumber/godog => github.com/laurazard/godog v0.0.0-20220922095256-4c4b17abdae7
// github.com/docker/buildx => github.com/docker/buildx v0.9.1
// github.com/docker/cli => github.com/docker/cli v20.10.3-0.20221013132413-1d6c6e2367e2+incompatible
// github.com/docker/compose/v2 => github.com/docker/compose/v2 v2.15.1
// github.com/docker/docker => github.com/moby/moby v20.10.3-0.20221021173910-5aac513617f0+incompatible
// github.com/moby/buildkit => github.com/moby/buildkit v0.10.1-0.20220816171719-55ba9d14360a
// )
replace (
github.com/compose-spec/compose-go => github.com/compose-spec/compose-go v1.0.8
github.com/docker/buildx => github.com/docker/buildx v0.5.2-0.20210422185057-908a856079fc
github.com/docker/cli => github.com/docker/cli v20.10.7+incompatible
github.com/docker/compose/v2 => github.com/docker/compose/v2 v2.2.0
github.com/docker/docker => github.com/docker/docker v20.10.3+incompatible
github.com/jaguilar/vt100 => github.com/tonistiigi/vt100 v0.0.0-20190402012908-ad4c4a574305
github.com/moby/buildkit => github.com/moby/buildkit v0.8.2-0.20210401015549-df49b648c8bf
github.com/tonistiigi/fsutil => github.com/tonistiigi/fsutil v0.0.0-20201103201449-0834f99b7b85
sigs.k8s.io/gateway-api => github.com/johnlanni/gateway-api v0.0.0-20231031082632-72137664e7c7
)

347
go.sum
View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: 1.2.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.2.0
version: 1.3.2

View File

@@ -2,6 +2,7 @@ apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "controller.name" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "controller.labels" . | nindent 4 }}
spec:
@@ -30,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 }}
@@ -74,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
@@ -183,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

@@ -2,6 +2,7 @@ apiVersion: v1
kind: Service
metadata:
name: {{ include "controller.name" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "controller.labels" . | nindent 4 }}
spec:

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.2.0
version: 1.3.2
- name: higress-console
repository: https://higress.io/helm-charts/
version: 1.2.0
digest: sha256:d53c2da70cb3bcace50bce756acb50750a84c0888ec0d4c112939bf3c6e4daeb
generated: "2023-09-22T16:52:34.940675+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.2.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.2.0
version: 1.3.2
- name: higress-console
repository: "https://higress.io/helm-charts/"
version: 1.2.0
version: 1.3.1
type: application
version: 1.2.0
version: 1.3.2

View File

@@ -0,0 +1,71 @@
diff -Naur istio/pilot/pkg/config/kube/gateway/conversion.go istio-new/pilot/pkg/config/kube/gateway/conversion.go
--- istio/pilot/pkg/config/kube/gateway/conversion.go 2023-09-25 17:26:32.000000000 +0800
+++ istio-new/pilot/pkg/config/kube/gateway/conversion.go 2023-09-25 17:25:27.000000000 +0800
@@ -656,6 +656,16 @@
Port: &istio.PortSelector{Number: uint32(*to.Port)},
}, nil
}
+ if equal((*string)(to.Group), "networking.higress.io") && nilOrEqual((*string)(to.Kind), "Service") {
+ var port *istio.PortSelector
+ if to.Port != nil {
+ port = &istio.PortSelector{Number: uint32(*to.Port)}
+ }
+ return &istio.Destination{
+ Host: string(to.Name),
+ Port: port,
+ }, nil
+ }
return nil, &ConfigError{
Reason: InvalidDestination,
Message: fmt.Sprintf("referencing unsupported backendRef: group %q kind %q", emptyIfNil((*string)(to.Group)), emptyIfNil((*string)(to.Kind))),
@@ -912,7 +922,7 @@
ObservedGeneration: obj.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(k8s.GatewayClassConditionStatusAccepted),
- Message: "Handled by Istio controller",
+ Message: "Handled by Higress controller",
})
return gcs
})
@@ -1371,6 +1381,10 @@
return d
}
+func equal(have *string, expected string) bool {
+ return have != nil && *have == expected
+}
+
func nilOrEqual(have *string, expected string) bool {
return have == nil || *have == expected
}
diff -Naur istio/pilot/pkg/leaderelection/leaderelection.go istio-new/pilot/pkg/leaderelection/leaderelection.go
--- istio/pilot/pkg/leaderelection/leaderelection.go 2023-09-25 17:26:31.000000000 +0800
+++ istio-new/pilot/pkg/leaderelection/leaderelection.go 2023-09-25 14:59:39.000000000 +0800
@@ -35,20 +35,20 @@
// Various locks used throughout the code
const (
- NamespaceController = "istio-namespace-controller-election"
- ServiceExportController = "istio-serviceexport-controller-election"
+ NamespaceController = "higress-namespace-controller-election"
+ ServiceExportController = "higress-serviceexport-controller-election"
// This holds the legacy name to not conflict with older control plane deployments which are just
// doing the ingress syncing.
- IngressController = "istio-leader"
+ IngressController = "higress-leader"
// GatewayStatusController controls the status of gateway.networking.k8s.io objects. For the v1alpha1
// this was formally "istio-gateway-leader"; because they are a different API group we need a different
// election to ensure we do not only handle one or the other.
- GatewayStatusController = "istio-gateway-status-leader"
+ GatewayStatusController = "higress-gateway-status-leader"
// GatewayDeploymentController controls the Deployment/Service generation from Gateways. This is
// separate from GatewayStatusController to allow running in a separate process (for low priv).
- GatewayDeploymentController = "istio-gateway-deployment-leader"
- StatusController = "istio-status-leader"
- AnalyzeController = "istio-analyze-leader"
+ GatewayDeploymentController = "higress-gateway-deployment-leader"
+ StatusController = "higress-status-leader"
+ AnalyzeController = "higress-analyze-leader"
)
var ClusterScopedNamespaceController = NamespaceController

View File

@@ -0,0 +1,90 @@
diff -Naur istio/pilot/pkg/config/kube/gateway/conversion.go istio-new/pilot/pkg/config/kube/gateway/conversion.go
--- istio/pilot/pkg/config/kube/gateway/conversion.go 2023-10-08 19:54:47.000000000 +0800
+++ istio-new/pilot/pkg/config/kube/gateway/conversion.go 2023-09-27 16:10:42.000000000 +0800
@@ -18,6 +18,7 @@
"fmt"
"regexp"
"sort"
+ "strconv"
"strings"
corev1 "k8s.io/api/core/v1"
@@ -176,7 +177,9 @@
hosts := hostnameToStringList(route.Hostnames)
for _, r := range route.Rules {
// TODO: implement rewrite, timeout, mirror, corspolicy, retries
- vs := &istio.HTTPRoute{}
+ vs := &istio.HTTPRoute{
+ Name: obj.Name,
+ }
for _, match := range r.Matches {
uri, err := createURIMatch(match)
if err != nil {
@@ -246,7 +249,9 @@
}}
}
- route, err := buildHTTPDestination(r.BackendRefs, obj.Namespace, domain, zero)
+ fallbackCluster := obj.Annotations["higress.io/fallback-service"]
+
+ route, err := buildHTTPDestination(r.BackendRefs, obj.Namespace, domain, zero, fallbackCluster)
if err != nil {
reportError(err)
return nil
@@ -581,11 +586,33 @@
return r
}
-func buildHTTPDestination(forwardTo []k8s.HTTPBackendRef, ns string, domain string, totalZero bool) ([]*istio.HTTPRouteDestination, *ConfigError) {
+func buildHTTPDestination(forwardTo []k8s.HTTPBackendRef, ns string, domain string, totalZero bool, fallbackCluster string) ([]*istio.HTTPRouteDestination, *ConfigError) {
if forwardTo == nil {
return nil, nil
}
+ var fallbackDest *istio.Destination
+ if fallbackCluster != "" {
+ var port uint64
+ host := fallbackCluster
+ colon := strings.LastIndex(fallbackCluster, ":")
+ if colon != -1 {
+ var err error
+ port, err = strconv.ParseUint(fallbackCluster[colon+1:], 10, 32)
+ if err == nil && port > 0 && port < 65536 {
+ host = fallbackCluster[:colon]
+ }
+ }
+ fallbackDest = &istio.Destination{
+ Host: host,
+ }
+ if port > 0 {
+ fallbackDest.Port = &istio.PortSelector{
+ Number: uint32(port),
+ }
+ }
+ }
+
weights := []int{}
action := []k8s.HTTPBackendRef{}
for i, w := range forwardTo {
@@ -612,6 +639,9 @@
Destination: dst,
Weight: int32(weights[i]),
}
+ if fallbackDest != nil {
+ rd.FallbackClusters = append(rd.FallbackClusters, fallbackDest)
+ }
for _, filter := range fwd.Filters {
switch filter.Type {
case k8s.HTTPRouteFilterRequestHeaderModifier:
diff -Naur istio/pilot/pkg/networking/core/v1alpha3/route/route.go istio-new/pilot/pkg/networking/core/v1alpha3/route/route.go
--- istio/pilot/pkg/networking/core/v1alpha3/route/route.go 2023-10-08 19:54:46.000000000 +0800
+++ istio-new/pilot/pkg/networking/core/v1alpha3/route/route.go 2023-09-27 16:18:16.000000000 +0800
@@ -669,7 +669,7 @@
}
var singleClusterConfig *fallback.ClusterFallbackConfig
var weightedClusterConfig *fallback.ClusterFallbackConfig
- isSupportFallback := supportFallback(node)
+ isSupportFallback := true
// Added by ingress
if len(in.Route) == 1 {
route := in.Route[0]

View File

@@ -0,0 +1,13 @@
diff -Naur istio/pilot/pkg/model/push_context.go istio-new/pilot/pkg/model/push_context.go
--- istio/pilot/pkg/model/push_context.go 2023-10-24 10:55:51.000000000 +0800
+++ istio-new/pilot/pkg/model/push_context.go 2023-10-20 17:00:06.000000000 +0800
@@ -704,6 +704,9 @@
if r.Destination != nil {
out = append(out, r.Destination.Host)
}
+ for _, d := range r.FallbackClusters {
+ out = append(out, d.Host)
+ }
}
if h.Mirror != nil {
out = append(out, h.Mirror.Host)

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,377 @@
diff -Naur istio/pilot/pkg/config/kube/gateway/conversion.go istio-new/pilot/pkg/config/kube/gateway/conversion.go
--- istio/pilot/pkg/config/kube/gateway/conversion.go 2023-11-03 17:18:56.000000000 +0800
+++ istio-new/pilot/pkg/config/kube/gateway/conversion.go 2023-11-03 17:14:50.000000000 +0800
@@ -151,15 +151,113 @@
}
}
+ // for gateway routes, build one VS per gateway+host
+ gatewayRoutes := make(map[string]map[string]*config.Config)
+
for _, obj := range r.HTTPRoute {
- if vsConfig := buildHTTPVirtualServices(obj, gatewayMap, r.Domain); vsConfig != nil {
+ buildHTTPVirtualServices(r, obj, gatewayMap, gatewayRoutes, r.Domain)
+ }
+ for _, vsByHost := range gatewayRoutes {
+ for _, vsConfig := range vsByHost {
result = append(result, *vsConfig)
}
}
return result
}
-func buildHTTPVirtualServices(obj config.Config, gateways map[parentKey]map[gatewayapiV1beta1.SectionName]*parentInfo, domain string) *config.Config {
+// getURIRank ranks a URI match type. Exact > Prefix > Regex
+func getURIRank(match *istio.HTTPMatchRequest) int {
+ if match.Uri == nil {
+ return -1
+ }
+ switch match.Uri.MatchType.(type) {
+ case *istio.StringMatch_Exact:
+ return 3
+ case *istio.StringMatch_Prefix:
+ return 2
+ case *istio.StringMatch_Regex:
+ // TODO optimize in new verison envoy
+ if strings.HasSuffix(match.Uri.GetRegex(), prefixMatchRegex) &&
+ !strings.ContainsAny(strings.TrimSuffix(match.Uri.GetRegex(), prefixMatchRegex), `\.+*?()|[]{}^$`) {
+ return 2
+ }
+ return 1
+ }
+ // should not happen
+ return -1
+}
+
+func getURILength(match *istio.HTTPMatchRequest) int {
+ if match.Uri == nil {
+ return 0
+ }
+ switch match.Uri.MatchType.(type) {
+ case *istio.StringMatch_Prefix:
+ return len(match.Uri.GetPrefix())
+ case *istio.StringMatch_Exact:
+ return len(match.Uri.GetExact())
+ case *istio.StringMatch_Regex:
+ return len(match.Uri.GetRegex())
+ }
+ // should not happen
+ return -1
+}
+
+// sortHTTPRoutes sorts generated vs routes to meet gateway-api requirements
+// see https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.HTTPRouteRule
+func sortHTTPRoutes(routes []*istio.HTTPRoute) {
+ sort.SliceStable(routes, func(i, j int) bool {
+ if len(routes[i].Match) == 0 {
+ return false
+ } else if len(routes[j].Match) == 0 {
+ return true
+ }
+ // Only look at match[0], we always generate only one match
+ m1, m2 := routes[i].Match[0], routes[j].Match[0]
+ r1, r2 := getURIRank(m1), getURIRank(m2)
+ len1, len2 := getURILength(m1), getURILength(m2)
+ switch {
+ // 1: Exact/Prefix/Regex
+ case r1 != r2:
+ return r1 > r2
+ case len1 != len2:
+ return len1 > len2
+ // 2: method math
+ case (m1.Method == nil) != (m2.Method == nil):
+ return m1.Method != nil
+ // 3: number of header matches
+ case len(m1.Headers) != len(m2.Headers):
+ return len(m1.Headers) > len(m2.Headers)
+ // 4: number of query matches
+ default:
+ return len(m1.QueryParams) > len(m2.QueryParams)
+ }
+ })
+}
+
+func routeMeta(obj config.Config) map[string]string {
+ m := parentMeta(obj, nil)
+ m[constants.InternalRouteSemantics] = constants.RouteSemanticsGateway
+ return m
+}
+
+func filteredReferences(parents []routeParentReference) []routeParentReference {
+ ret := make([]routeParentReference, 0, len(parents))
+ for _, p := range parents {
+ if p.DeniedReason != nil {
+ // We should filter this out
+ continue
+ }
+ ret = append(ret, p)
+ }
+ // To ensure deterministic order, sort them
+ sort.Slice(ret, func(i, j int) bool {
+ return ret[i].InternalName < ret[j].InternalName
+ })
+ return ret
+}
+
+func buildHTTPVirtualServices(ctx *KubernetesResources, obj config.Config, gateways map[parentKey]map[gatewayapiV1beta1.SectionName]*parentInfo, gatewayRoutes map[string]map[string]*config.Config, domain string) {
route := obj.Spec.(*gatewayapiV1beta1.HTTPRouteSpec)
parentRefs := extractParentReferenceInfo(gateways, route.ParentRefs, route.Hostnames, gvk.HTTPRoute, obj.Namespace)
@@ -172,10 +270,7 @@
})
}
- name := fmt.Sprintf("%s-%s", obj.Name, constants.KubernetesGatewayName)
-
httproutes := []*istio.HTTPRoute{}
- hosts := hostnameToStringList(route.Hostnames)
for _, r := range route.Rules {
// TODO: implement rewrite, timeout, mirror, corspolicy, retries
vs := &istio.HTTPRoute{
@@ -185,22 +280,22 @@
uri, err := createURIMatch(match)
if err != nil {
reportError(err)
- return nil
+ return
}
headers, err := createHeadersMatch(match)
if err != nil {
reportError(err)
- return nil
+ return
}
qp, err := createQueryParamsMatch(match)
if err != nil {
reportError(err)
- return nil
+ return
}
method, err := createMethodMatch(match)
if err != nil {
reportError(err)
- return nil
+ return
}
vs.Match = append(vs.Match, &istio.HTTPMatchRequest{
Uri: uri,
@@ -219,7 +314,7 @@
mirror, err := createMirrorFilter(filter.RequestMirror, obj.Namespace, domain)
if err != nil {
reportError(err)
- return nil
+ return
}
vs.Mirror = mirror
default:
@@ -227,7 +322,7 @@
Reason: InvalidFilter,
Message: fmt.Sprintf("unsupported filter type %q", filter.Type),
})
- return nil
+ return
}
}
@@ -255,33 +350,65 @@
route, err := buildHTTPDestination(r.BackendRefs, obj.Namespace, domain, zero, fallbackCluster)
if err != nil {
reportError(err)
- return nil
+ return
}
vs.Route = route
httproutes = append(httproutes, vs)
}
reportError(nil)
- gatewayNames := referencesToInternalNames(parentRefs)
- if len(gatewayNames) == 0 {
- return nil
+
+ count := 0
+ for _, parent := range filteredReferences(parentRefs) {
+ // for gateway routes, build one VS per gateway+host
+ routeMap := gatewayRoutes
+ routeKey := parent.InternalName
+ vsHosts := hostnameToStringList(route.Hostnames)
+ routes := httproutes
+ if len(routes) == 0 {
+ continue
+ }
+ if _, f := routeMap[routeKey]; !f {
+ routeMap[routeKey] = make(map[string]*config.Config)
+ }
+
+ // Create one VS per hostname with a single hostname.
+ // This ensures we can treat each hostname independently, as the spec requires
+ for _, h := range vsHosts {
+ if cfg := routeMap[routeKey][h]; cfg != nil {
+ // merge http routes
+ vs := cfg.Spec.(*istio.VirtualService)
+ vs.Http = append(vs.Http, routes...)
+ // append parents
+ cfg.Annotations[constants.InternalParentNames] = fmt.Sprintf("%s,%s/%s.%s",
+ cfg.Annotations[constants.InternalParentNames], obj.GroupVersionKind.Kind, obj.Name, obj.Namespace)
+ } else {
+ name := fmt.Sprintf("%s-%d-%s", obj.Name, count, constants.KubernetesGatewayName)
+ routeMap[routeKey][h] = &config.Config{
+ Meta: config.Meta{
+ CreationTimestamp: obj.CreationTimestamp,
+ GroupVersionKind: gvk.VirtualService,
+ Name: name,
+ Annotations: routeMeta(obj),
+ Namespace: obj.Namespace,
+ Domain: ctx.Domain,
+ },
+ Spec: &istio.VirtualService{
+ Hosts: []string{h},
+ Gateways: []string{parent.InternalName},
+ Http: routes,
+ },
+ }
+ count++
+ }
+ }
}
- vsConfig := config.Config{
- Meta: config.Meta{
- CreationTimestamp: obj.CreationTimestamp,
- GroupVersionKind: gvk.VirtualService,
- Name: name,
- Annotations: parentMeta(obj, nil),
- Namespace: obj.Namespace,
- Domain: domain,
- },
- Spec: &istio.VirtualService{
- Hosts: hosts,
- Gateways: gatewayNames,
- Http: httproutes,
- },
+ for _, vsByHost := range gatewayRoutes {
+ for _, cfg := range vsByHost {
+ vs := cfg.Spec.(*istio.VirtualService)
+ sortHTTPRoutes(vs.Http)
+ }
}
- return &vsConfig
}
func parentMeta(obj config.Config, sectionName *gatewayapiV1beta1.SectionName) map[string]string {
@@ -1155,9 +1282,11 @@
}
gs.Addresses = make([]gatewayapiV1beta1.GatewayAddress, 0, len(addressesToReport))
for _, addr := range addressesToReport {
+ addrPairs := strings.Split(addr, ":")
gs.Addresses = append(gs.Addresses, gatewayapiV1beta1.GatewayAddress{
- Type: &addrType,
- Value: addr,
+ Type: &addrType,
+ // strip the port
+ Value: addrPairs[0],
})
}
return gs
diff -Naur istio/pilot/pkg/model/push_context.go istio-new/pilot/pkg/model/push_context.go
--- istio/pilot/pkg/model/push_context.go 2023-11-03 17:18:56.000000000 +0800
+++ istio-new/pilot/pkg/model/push_context.go 2023-11-03 17:05:47.000000000 +0800
@@ -841,7 +841,19 @@
func (ps *PushContext) VirtualServicesForGateway(proxy *Proxy, gateway string) []config.Config {
res := ps.virtualServiceIndex.privateByNamespaceAndGateway[proxy.ConfigNamespace][gateway]
res = append(res, ps.virtualServiceIndex.exportedToNamespaceByGateway[proxy.ConfigNamespace][gateway]...)
- res = append(res, ps.virtualServiceIndex.publicByGateway[gateway]...)
+
+ // Favor same-namespace Gateway routes, to give the "consumer override" preference.
+ // We do 2 iterations here to avoid extra allocations.
+ for _, vs := range ps.virtualServiceIndex.publicByGateway[gateway] {
+ if UseGatewaySemantics(vs) && vs.Namespace == proxy.ConfigNamespace {
+ res = append(res, vs)
+ }
+ }
+ for _, vs := range ps.virtualServiceIndex.publicByGateway[gateway] {
+ if !(UseGatewaySemantics(vs) && vs.Namespace == proxy.ConfigNamespace) {
+ res = append(res, vs)
+ }
+ }
return res
}
diff -Naur istio/pilot/pkg/model/virtualservice.go istio-new/pilot/pkg/model/virtualservice.go
--- istio/pilot/pkg/model/virtualservice.go 2023-11-03 17:18:55.000000000 +0800
+++ istio-new/pilot/pkg/model/virtualservice.go 2023-11-03 15:19:08.000000000 +0800
@@ -76,6 +76,11 @@
}
func resolveVirtualServiceShortnames(rule *networking.VirtualService, meta config.Meta) {
+ // Kubernetes Gateway API semantics support shortnames
+ // if UseGatewaySemantics(config.Config{Meta: meta}) {
+ // return
+ // }
+
// resolve top level hosts
for i, h := range rule.Hosts {
rule.Hosts[i] = string(ResolveShortnameToFQDN(h, meta))
@@ -524,3 +529,10 @@
}
return false
}
+
+// UseGatewaySemantics determines which logic we should use for VirtualService
+// This allows gateway-api and VS to both be represented by VirtualService, but have different
+// semantics.
+func UseGatewaySemantics(cfg config.Config) bool {
+ return cfg.Annotations[constants.InternalRouteSemantics] == constants.RouteSemanticsGateway
+}
diff -Naur istio/pilot/pkg/networking/core/v1alpha3/route/route.go istio-new/pilot/pkg/networking/core/v1alpha3/route/route.go
--- istio/pilot/pkg/networking/core/v1alpha3/route/route.go 2023-11-03 17:18:56.000000000 +0800
+++ istio-new/pilot/pkg/networking/core/v1alpha3/route/route.go 2023-11-03 17:05:55.000000000 +0800
@@ -408,7 +408,6 @@
break
}
}
-
if len(out) == 0 {
return nil, fmt.Errorf("no routes matched")
}
@@ -493,6 +492,14 @@
},
}
+ if model.UseGatewaySemantics(virtualService) {
+ if uri, isPrefixReplace := cutPrefix(redirect.Uri, "%PREFIX()%"); isPrefixReplace {
+ action.Redirect.PathRewriteSpecifier = &route.RedirectAction_PrefixRewrite{
+ PrefixRewrite: uri,
+ }
+ }
+ }
+
if redirect.Scheme != "" {
action.Redirect.SchemeRewriteSpecifier = &route.RedirectAction_SchemeRedirect{SchemeRedirect: redirect.Scheme}
}
@@ -1616,3 +1623,10 @@
isSupport = curVersion.GreaterThan(notSupportFallback)
return
}
+
+func cutPrefix(s, prefix string) (after string, found bool) {
+ if !strings.HasPrefix(s, prefix) {
+ return s, false
+ }
+ return s[len(prefix):], true
+}
diff -Naur istio/pkg/config/constants/constants.go istio-new/pkg/config/constants/constants.go
--- istio/pkg/config/constants/constants.go 2023-11-03 17:18:54.000000000 +0800
+++ istio-new/pkg/config/constants/constants.go 2023-11-03 14:29:27.000000000 +0800
@@ -15,6 +15,12 @@
package constants
const (
+ InternalParentNames = "internal.istio.io/parents"
+
+ InternalRouteSemantics = "internal.istio.io/route-semantics"
+
+ RouteSemanticsGateway = "gateway"
+
// UnspecifiedIP constant for empty IP address
UnspecifiedIP = "0.0.0.0"

View File

@@ -0,0 +1,50 @@
diff -Naur istio/pilot/pkg/config/kube/gateway/conversion.go istio-new/pilot/pkg/config/kube/gateway/conversion.go
--- istio/pilot/pkg/config/kube/gateway/conversion.go 2023-11-03 20:09:38.000000000 +0800
+++ istio-new/pilot/pkg/config/kube/gateway/conversion.go 2023-11-03 20:02:26.000000000 +0800
@@ -165,6 +165,34 @@
return result
}
+// isCatchAll returns true if HTTPMatchRequest is a catchall match otherwise
+// false. Note - this may not be exactly "catch all" as we don't know the full
+// class of possible inputs As such, this is used only for optimization.
+func isCatchAllMatch(m *istio.HTTPMatchRequest) bool {
+ catchall := false
+ if m.Uri != nil {
+ switch m := m.Uri.MatchType.(type) {
+ case *istio.StringMatch_Prefix:
+ catchall = m.Prefix == "/"
+ case *istio.StringMatch_Regex:
+ catchall = m.Regex == "*"
+ }
+ }
+ // A Match is catch all if and only if it has no match set
+ // and URI has a prefix / or regex *.
+ return catchall &&
+ len(m.Headers) == 0 &&
+ len(m.QueryParams) == 0 &&
+ len(m.SourceLabels) == 0 &&
+ len(m.WithoutHeaders) == 0 &&
+ len(m.Gateways) == 0 &&
+ m.Method == nil &&
+ m.Scheme == nil &&
+ m.Port == 0 &&
+ m.Authority == nil &&
+ m.SourceNamespace == ""
+}
+
// getURIRank ranks a URI match type. Exact > Prefix > Regex
func getURIRank(match *istio.HTTPMatchRequest) int {
if match.Uri == nil {
@@ -212,6 +240,11 @@
} else if len(routes[j].Match) == 0 {
return true
}
+ if isCatchAllMatch(routes[i].Match[0]) {
+ return false
+ } else if isCatchAllMatch(routes[j].Match[0]) {
+ return true
+ }
// Only look at match[0], we always generate only one match
m1, m2 := routes[i].Match[0], routes[j].Match[0]
r1, r2 := getURIRank(m1), getURIRank(m2)

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,25 +15,31 @@
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"
)
var (
listenPort = 0
promPort = 0
grafanaPort = 0
consolePort = 0
listenPort = 0
promPort = 0
grafanaPort = 0
consolePort = 0
controllerPort = 0
bindAddress = "localhost"
@@ -48,12 +54,15 @@ var (
envoyDashNs = ""
proxyAdminPort int
docker = false
)
const (
defaultPrometheusPort = 9090
defaultGrafanaPort = 3000
defaultConsolePort = 8080
defaultControllerPort = 8888
)
func newDashboardCmd() *cobra.Command {
@@ -79,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.")
@@ -89,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.")
@@ -97,8 +107,14 @@ 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()
controllerDebugCmd.PersistentFlags().IntVar(&controllerPort, "ui-port", defaultControllerPort, "The component dashboard UI port.")
dashboardCmd.AddCommand(controllerDebugCmd)
flags := dashboardCmd.PersistentFlags()
options.AddKubeConfigFlags(flags)
return dashboardCmd
}
@@ -149,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
@@ -172,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{
@@ -265,6 +312,41 @@ func envoyDashCmd() *cobra.Command {
return cmd
}
// port-forward to Higress System Console; open browser
func controllerDebugCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "controller",
Short: "Open Controller debug web UI",
Long: `Open Higress Controller`,
Example: ` hgctl dashboard controller
# with short syntax
hgctl dash controller
hgctl d controller`,
RunE: func(cmd *cobra.Command, args []string) error {
client, err := kubernetes.NewCLIClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
if err != nil {
return fmt.Errorf("build CLI client fail: %w", err)
}
pl, err := client.PodsForSelector(addonNamespace, "app=higress-controller")
if err != nil {
return fmt.Errorf("not able to locate controller pod: %v", err)
}
if len(pl.Items) < 1 {
return errors.New("no higress controller pods found")
}
// only use the first pod in the list
return portForward(pl.Items[0].Name, addonNamespace, "Controller",
"http://%s/debug", bindAddress, controllerPort, client, cmd.OutOrStdout(), browser)
},
}
return cmd
}
// portForward first tries to forward localhost:remotePort to podName:remotePort, falls back to dynamic local port
func portForward(podName, namespace, flavor, urlFormat, localAddress string, remotePort int,
client kubernetes.CLIClient, writer io.Writer, browser bool,
@@ -282,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)
}
@@ -319,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 {
@@ -330,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

@@ -0,0 +1,111 @@
// 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 docker
import (
"context"
"io"
"strings"
"github.com/compose-spec/compose-go/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/flags"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/compose"
)
type Compose struct {
client *api.ServiceProxy
w io.Writer
}
func NewCompose(w io.Writer) (*Compose, error) {
c := &Compose{w: w}
dockerCli, err := command.NewDockerCli(
command.WithCombinedStreams(c.w),
// command.WithDefaultContextStoreConfig(), Deprecated, set during NewDockerCli
)
if err != nil {
return nil, err
}
opts := flags.NewClientOptions()
err = dockerCli.Initialize(opts)
if err != nil {
return nil, err
}
c.client = api.NewServiceProxy().WithService(compose.NewComposeService(dockerCli.Client(), dockerCli.ConfigFile()))
return c, nil
}
func (c Compose) Up(ctx context.Context, name string, configs []string, source string, detach bool) error {
pOpts, err := cli.NewProjectOptions(
configs,
cli.WithWorkingDirectory(source),
cli.WithDefaultConfigPath,
cli.WithName(name),
)
if err != nil {
return err
}
project, err := cli.ProjectFromOptions(pOpts)
if err != nil {
return err
}
for i, s := range project.Services {
// TODO(WeixinX): Change from `Label` to `CustomLabels` after upgrading the dependency library github.com/compose-spec/compose-go
s.Labels = map[string]string{
api.ProjectLabel: project.Name,
api.ServiceLabel: s.Name,
api.VersionLabel: api.ComposeVersion,
api.WorkingDirLabel: project.WorkingDir,
api.ConfigFilesLabel: strings.Join(project.ComposeFiles, ","),
api.OneoffLabel: "False",
}
project.Services[i] = s
}
project.WithoutUnnecessaryResources()
// for log
var consumer api.LogConsumer
if !detach {
// TODO(WeixinX): Change to `formatter.NewLogConsumer(ctx, c.w, c.w, true, true, false)` after upgrading the dependency library github.com/compose-spec/compose-go
consumer = formatter.NewLogConsumer(ctx, c.w, true, true)
}
attachTo := make([]string, 0)
for _, svc := range project.Services {
attachTo = append(attachTo, svc.Name)
}
return c.client.Up(ctx, project, api.UpOptions{
Start: api.StartOptions{
Attach: consumer,
AttachTo: attachTo,
},
})
}
func (c Compose) List(ctx context.Context) ([]api.Stack, error) {
return c.client.List(ctx, api.ListOptions{})
}
func (c Compose) Down(ctx context.Context, name string) error {
return c.client.Down(ctx, name, api.DownOptions{})
}

View File

@@ -26,15 +26,40 @@ import (
"sigs.k8s.io/yaml"
)
// ReadYamlProfile gets the overlay yaml file from list of files and return profile value from file overlay and set overlay.
func ReadYamlProfile(inFilenames []string, setFlags []string) (string, string, error) {
// GetProfileFromFlags get profile name from flags.
func GetProfileFromFlags(setFlags []string) (string, error) {
profileName := DefaultProfileName
// The profile coming from --set flag has the highest precedence.
psf := GetValueForSetFlag(setFlags, "profile")
if psf != "" {
profileName = psf
}
return "", profileName, nil
return profileName, nil
}
func GetValuesOverylayFromFiles(inFilenames []string) (string, error) {
// Convert layeredYamls under values node in profile file to support helm values
overLayYamls := ""
// Get Overlays from files
if len(inFilenames) > 0 {
layeredYamls, err := ReadLayeredYAMLs(inFilenames)
if err != nil {
return "", err
}
vals := make(map[string]any)
if err := yaml.Unmarshal([]byte(layeredYamls), &vals); err != nil {
return "", fmt.Errorf("%s:\n\nYAML:\n%s", err, layeredYamls)
}
values := make(map[string]any)
values["values"] = vals
out, err := yaml.Marshal(values)
if err != nil {
return "", err
}
overLayYamls = string(out)
}
return overLayYamls, nil
}
func GetUninstallProfileName() string {
@@ -101,12 +126,17 @@ func GenerateConfig(inFilenames []string, setFlags []string) (string, *Profile,
return "", nil, "", err
}
fy, profileName, err := ReadYamlProfile(inFilenames, setFlags)
profileName, err := GetProfileFromFlags(setFlags)
if err != nil {
return "", nil, "", err
}
profileString, profile, err := GenProfile(profileName, fy, setFlags)
valuesOverlay, err := GetValuesOverylayFromFiles(inFilenames)
if err != nil {
return "", nil, "", err
}
profileString, profile, err := GenProfile(profileName, valuesOverlay, setFlags)
if err != nil {
return "", nil, "", err
@@ -288,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

@@ -17,7 +17,9 @@ package helm
import (
"errors"
"fmt"
"strings"
"istio.io/istio/operator/pkg/util"
"sigs.k8s.io/yaml"
)
@@ -33,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"`
@@ -43,83 +46,70 @@ type Profile struct {
}
type ProfileGlobal struct {
Install InstallMode `json:"install,omitempty"`
IngressClass string `json:"ingressClass,omitempty"`
WatchNamespace string `json:"watchNamespace,omitempty"`
DisableAlpnH2 bool `json:"disableAlpnH2,omitempty"`
EnableStatus bool `json:"enableStatus,omitempty"`
EnableIstioAPI bool `json:"enableIstioAPI,omitempty"`
Namespace string `json:"namespace,omitempty"`
IstioNamespace string `json:"istioNamespace,omitempty"`
Install InstallMode `json:"install,omitempty"`
IngressClass string `json:"ingressClass,omitempty"`
EnableIstioAPI bool `json:"enableIstioAPI,omitempty"`
EnableGatewayAPI bool `json:"enableGatewayAPI,omitempty"`
Namespace string `json:"namespace,omitempty"`
}
func (p ProfileGlobal) SetFlags(install InstallMode) ([]string, error) {
sets := make([]string, 0)
sets = append(sets, fmt.Sprintf("global.ingressClass=%s", p.IngressClass))
sets = append(sets, fmt.Sprintf("global.watchNamespace=%s", p.WatchNamespace))
sets = append(sets, fmt.Sprintf("global.disableAlpnH2=%t", p.DisableAlpnH2))
sets = append(sets, fmt.Sprintf("global.enableStatus=%t", p.EnableStatus))
sets = append(sets, fmt.Sprintf("global.enableIstioAPI=%t", p.EnableIstioAPI))
sets = append(sets, fmt.Sprintf("global.istioNamespace=%s", p.IstioNamespace))
if install == InstallLocalK8s {
sets = append(sets, fmt.Sprintf("global.local=%t", true))
if install == InstallK8s || install == InstallLocalK8s {
sets = append(sets, fmt.Sprintf("global.ingressClass=%s", p.IngressClass))
sets = append(sets, fmt.Sprintf("global.enableIstioAPI=%t", p.EnableIstioAPI))
sets = append(sets, fmt.Sprintf("global.enableGatewayAPI=%t", p.EnableGatewayAPI))
if install == InstallLocalK8s {
sets = append(sets, fmt.Sprintf("global.local=%t", true))
}
}
return sets, nil
}
func (p ProfileGlobal) Validate(install InstallMode) []error {
errs := make([]error, 0)
// now only support k8s and local-k8s installation mode
if p.Install != InstallK8s && p.Install != InstallLocalK8s {
errs = append(errs, errors.New("global.install only can be set to k8s or local-k8s"))
// now only support k8s, local-k8s, local-docker installation mode
if install != InstallK8s && install != InstallLocalK8s && install != InstallLocalDocker {
errs = append(errs, errors.New("global.install only can be set to k8s, local-k8s or local-docker"))
}
if len(p.IngressClass) == 0 {
errs = append(errs, errors.New("global.ingressClass can't be empty"))
}
if len(p.Namespace) == 0 {
errs = append(errs, errors.New("global.namespace can't be empty"))
}
if len(p.IstioNamespace) == 0 {
errs = append(errs, errors.New("global.istioNamespace can't be empty"))
if install == InstallK8s || install == InstallLocalK8s {
if len(p.IngressClass) == 0 {
errs = append(errs, errors.New("global.ingressClass can't be empty"))
}
if len(p.Namespace) == 0 {
errs = append(errs, errors.New("global.namespace can't be empty"))
}
}
return errs
}
type ProfileConsole struct {
Port uint32 `json:"port,omitempty"`
Replicas uint32 `json:"replicas,omitempty"`
ServiceType string `json:"serviceType,omitempty"`
Domain string `json:"domain,omitempty"`
TlsSecretName string `json:"tlsSecretName,omitempty"`
WebLoginPrompt string `json:"webLoginPrompt,omitempty"`
AdminPasswordValue string `json:"adminPasswordValue,omitempty"`
AdminPasswordLength uint32 `json:"adminPasswordLength,omitempty"`
O11yEnabled bool `json:"o11YEnabled,omitempty"`
PvcRwxSupported bool `json:"pvcRwxSupported,omitempty"`
Port uint32 `json:"port,omitempty"`
Replicas uint32 `json:"replicas,omitempty"`
O11yEnabled bool `json:"o11YEnabled,omitempty"`
}
func (p ProfileConsole) SetFlags(install InstallMode) ([]string, error) {
sets := make([]string, 0)
sets = append(sets, fmt.Sprintf("higress-console.replicaCount=%d", p.Replicas))
sets = append(sets, fmt.Sprintf("higress-console.service.type=%s", p.ServiceType))
sets = append(sets, fmt.Sprintf("higress-console.domain=%s", p.Domain))
sets = append(sets, fmt.Sprintf("higress-console.tlsSecretName=%s", p.TlsSecretName))
sets = append(sets, fmt.Sprintf("higress-console.web.login.prompt=%s", p.WebLoginPrompt))
sets = append(sets, fmt.Sprintf("higress-console.admin.password.value=%s", p.AdminPasswordValue))
sets = append(sets, fmt.Sprintf("higress-console.admin.password.length=%d", p.AdminPasswordLength))
sets = append(sets, fmt.Sprintf("higress-console.o11y.enabled=%t", p.O11yEnabled))
sets = append(sets, fmt.Sprintf("higress-console.pvc.rwxSupported=%t", p.PvcRwxSupported))
if install == InstallK8s || install == InstallLocalK8s {
sets = append(sets, fmt.Sprintf("higress-console.replicaCount=%d", p.Replicas))
sets = append(sets, fmt.Sprintf("higress-console.o11y.enabled=%t", p.O11yEnabled))
}
return sets, nil
}
func (p ProfileConsole) Validate(install InstallMode) []error {
errs := make([]error, 0)
if p.Replicas <= 0 {
errs = append(errs, errors.New("console.replica need be large than zero"))
if install == InstallK8s || install == InstallLocalK8s {
if p.Replicas <= 0 {
errs = append(errs, errors.New("console.replica need be large than zero"))
}
}
if p.ServiceType != "ClusterIP" && p.ServiceType != "NodePort" && p.ServiceType != "LoadBalancer" {
errs = append(errs, errors.New("console.serviceType can only be set to ClusterIP, NodePort or LoadBalancer"))
if install == InstallLocalDocker {
if p.Port <= 0 {
errs = append(errs, errors.New("console.port need be large than zero"))
}
}
return errs
@@ -134,16 +124,31 @@ type ProfileGateway struct {
func (p ProfileGateway) SetFlags(install InstallMode) ([]string, error) {
sets := make([]string, 0)
sets = append(sets, fmt.Sprintf("higress-core.gateway.replicas=%d", p.Replicas))
if install == InstallK8s || install == InstallLocalK8s {
sets = append(sets, fmt.Sprintf("higress-core.gateway.replicas=%d", p.Replicas))
}
return sets, nil
}
func (p ProfileGateway) Validate(install InstallMode) []error {
errs := make([]error, 0)
if p.Replicas <= 0 {
errs = append(errs, errors.New("gateway.replica need be large than zero"))
if install == InstallK8s || install == InstallLocalK8s {
if p.Replicas <= 0 {
errs = append(errs, errors.New("gateway.replica need be large than zero"))
}
}
if install == InstallLocalDocker {
if p.HttpPort <= 0 {
errs = append(errs, errors.New("gateway.httpPort need be large than zero"))
}
if p.HttpsPort <= 0 {
errs = append(errs, errors.New("gateway.httpsPort need be large than zero"))
}
if p.MetricsPort <= 0 {
errs = append(errs, errors.New("gateway.MetricsPort need be large than zero"))
}
}
return errs
}
@@ -153,16 +158,19 @@ type ProfileController struct {
func (p ProfileController) SetFlags(install InstallMode) ([]string, error) {
sets := make([]string, 0)
sets = append(sets, fmt.Sprintf("higress-core.controller.replicas=%d", p.Replicas))
if install == InstallK8s || install == InstallLocalK8s {
sets = append(sets, fmt.Sprintf("higress-core.controller.replicas=%d", p.Replicas))
}
return sets, nil
}
func (p ProfileController) Validate(install InstallMode) []error {
errs := make([]error, 0)
if p.Replicas <= 0 {
errs = append(errs, errors.New("controller.replica need be large than zero"))
if install == InstallK8s || install == InstallLocalK8s {
if p.Replicas <= 0 {
errs = append(errs, errors.New("controller.replica need be large than zero"))
}
}
return errs
}
@@ -176,6 +184,31 @@ type ProfileStorage struct {
func (p ProfileStorage) Validate(install InstallMode) []error {
errs := make([]error, 0)
if install == InstallLocalDocker {
if len(p.Url) == 0 {
errs = append(errs, errors.New("storage.url can't be empty"))
}
if len(p.Ns) == 0 {
errs = append(errs, errors.New("storage.ns can't be empty"))
}
if !strings.HasPrefix(p.Url, "nacos://") && !strings.HasPrefix(p.Url, "file://") {
errs = append(errs, fmt.Errorf("invalid storage url: %s", p.Url))
} else {
// check localhost or 127.0.0.0
if strings.Contains(p.Url, "localhost") || strings.Contains(p.Url, "/127.") {
errs = append(errs, errors.New("localhost or loopback addresses in nacos url won't work"))
}
}
if len(p.DataEncKey) > 0 && len(p.DataEncKey) != 32 {
errs = append(errs, fmt.Errorf("expecting 32 characters for dataEncKey, but got %d length", len(p.DataEncKey)))
}
if len(p.Username) > 0 && len(p.Password) == 0 || len(p.Username) == 0 && len(p.Password) > 0 {
errs = append(errs, errors.New("both nacos username and password should be provided"))
}
}
return errs
}
@@ -186,8 +219,8 @@ type Chart struct {
}
type ProfileCharts struct {
Higress Chart `json:"higress,omitempty"`
Istio Chart `json:"istio,omitempty"`
Higress Chart `json:"higress,omitempty"`
Standalone Chart `json:"standalone,omitempty"`
}
func (p ProfileCharts) Validate(install InstallMode) []error {
@@ -222,8 +255,13 @@ func (p *Profile) ValuesYaml() (string, error) {
}
valueOverlayYAML = string(out)
}
flagsYAML, err := overlaySetFlagValues("", setFlags)
if err != nil {
return "", err
}
// merge values and setFlags
overlayYAML, err := overlaySetFlagValues(valueOverlayYAML, setFlags)
overlayYAML, err := util.OverlayYAML(flagsYAML, valueOverlayYAML)
if err != nil {
return "", err
}
@@ -237,6 +275,26 @@ func (p *Profile) IstioEnabled() bool {
return false
}
func (p *Profile) GatewayAPIEnabled() bool {
if (p.Global.Install == InstallK8s || p.Global.Install == InstallLocalK8s) && p.Global.EnableGatewayAPI {
return true
}
return false
}
func (p *Profile) GetIstioNamespace() string {
if valuesGlobal, ok1 := p.Values["global"]; ok1 {
if global, ok2 := valuesGlobal.(map[string]any); ok2 {
if istioNamespace, ok3 := global["istioNamespace"]; ok3 {
if namespace, ok4 := istioNamespace.(string); ok4 {
return namespace
}
}
}
}
return ""
}
func (p *Profile) Validate() error {
errs := make([]error, 0)
errsGlobal := p.Global.Validate(p.Global.Install)
@@ -256,7 +314,7 @@ func (p *Profile) Validate() error {
errs = append(errs, errsController...)
}
errsStorage := p.Storage.Validate(p.Global.Install)
if len(errsController) > 0 {
if len(errsStorage) > 0 {
errs = append(errs, errsStorage...)
}
errsCharts := p.Charts.Validate(p.Global.Install)

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"
)
@@ -127,13 +128,19 @@ type RendererOptions struct {
Name string
Namespace string
// fields for LocalRenderer
// fields for LocalChartRenderer and LocalFileRenderer
FS fs.FS
Dir string
// fields for RemoteRenderer
Version string
RepoURL string
// Capabilities
Capabilities *chartutil.Capabilities
// rest config
restConfig *rest.Config
}
type RendererOption func(*RendererOptions)
@@ -174,14 +181,96 @@ func WithRepoURL(repo string) RendererOption {
}
}
// LocalRenderer load chart from local file system
type LocalRenderer struct {
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
filesMap map[string]string
Started bool
}
func NewLocalFileRenderer(opts ...RendererOption) (Renderer, error) {
newOpts := &RendererOptions{}
for _, opt := range opts {
opt(newOpts)
}
return &LocalFileRenderer{
Opts: newOpts,
filesMap: make(map[string]string),
}, nil
}
func (l *LocalFileRenderer) Init() error {
fileNames, err := getFileNames(l.Opts.FS, l.Opts.Dir)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("chart of component %s doesn't exist", l.Opts.Name)
}
return fmt.Errorf("getFileNames err: %s", err)
}
for _, fileName := range fileNames {
data, err := fs.ReadFile(l.Opts.FS, fileName)
if err != nil {
return fmt.Errorf("ReadFile %s err: %s", fileName, err)
}
l.filesMap[fileName] = string(data)
}
l.Started = true
return nil
}
func (l *LocalFileRenderer) RenderManifest(valsYaml string) (string, error) {
if !l.Started {
return "", errors.New("LocalFileRenderer has not been init")
}
keys := make([]string, 0, len(l.filesMap))
for key := range l.filesMap {
keys = append(keys, key)
}
// to ensure that every manifest rendered by same values are the same
sort.Strings(keys)
var builder strings.Builder
for i := 0; i < len(keys); i++ {
file := l.filesMap[keys[i]]
file = util.ApplyFilters(file, DefaultFilters...)
// ignore empty manifest
if file == "" {
continue
}
if !strings.HasSuffix(file, YAMLSeparator) {
file += YAMLSeparator
}
builder.WriteString(file)
}
return builder.String(), nil
}
func (l *LocalFileRenderer) SetVersion(version string) {
l.Opts.Version = version
}
// LocalChartRenderer load chart from local file system
type LocalChartRenderer struct {
Opts *RendererOptions
Chart *chart.Chart
Started bool
}
func (lr *LocalRenderer) Init() error {
func (lr *LocalChartRenderer) Init() error {
fileNames, err := getFileNames(lr.Opts.FS, lr.Opts.Dir)
if err != nil {
if os.IsNotExist(err) {
@@ -212,18 +301,18 @@ func (lr *LocalRenderer) Init() error {
return nil
}
func (lr *LocalRenderer) RenderManifest(valsYaml string) (string, error) {
func (lr *LocalChartRenderer) RenderManifest(valsYaml string) (string, error) {
if !lr.Started {
return "", errors.New("LocalRenderer has not been init")
return "", errors.New("LocalChartRenderer has not been init")
}
return renderManifest(valsYaml, lr.Chart, true, lr.Opts, DefaultFilters...)
}
func (lr *LocalRenderer) SetVersion(version string) {
func (lr *LocalChartRenderer) SetVersion(version string) {
lr.Opts.Version = version
}
func NewLocalRenderer(opts ...RendererOption) (Renderer, error) {
func NewLocalChartRenderer(opts ...RendererOption) (Renderer, error) {
newOpts := &RendererOptions{}
for _, opt := range opts {
opt(newOpts)
@@ -232,7 +321,7 @@ func NewLocalRenderer(opts ...RendererOption) (Renderer, error) {
if err := verifyRendererOptions(newOpts); err != nil {
return nil, fmt.Errorf("verify err: %s", err)
}
return &LocalRenderer{
return &LocalChartRenderer{
Opts: newOpts,
}, nil
}
@@ -348,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 {
@@ -358,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,11 +17,11 @@ 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/kubernetes"
"github.com/alibaba/higress/pkg/cmd/options"
"github.com/spf13/cobra"
)
@@ -32,13 +32,16 @@ const (
// manifestsFlagHelpStr is the command line description for --manifests
manifestsFlagHelpStr = `Specify a path to a directory of profiles
(e.g. ~/Downloads/higress/manifests).`
outputHelpstr = "Specify a file to write profile yaml"
filenameFlagHelpStr = "Path to file containing helm custom values"
outputHelpstr = "Specify a file to write profile yaml"
profileNameK8s = "k8s"
profileNameLocalK8s = "local-k8s"
profileNameK8s = "k8s"
profileNameLocalK8s = "local-k8s"
profileNameLocalDocker = "local-docker"
)
type InstallArgs struct {
// InFilenames is a filename to helm custom values
InFilenames []string
// KubeConfigPath is the path to kube config file.
KubeConfigPath string
@@ -61,6 +64,7 @@ func (a *InstallArgs) String() string {
}
func addInstallFlags(cmd *cobra.Command, args *InstallArgs) {
cmd.PersistentFlags().StringSliceVarP(&args.InFilenames, "filename", "f", nil, filenameFlagHelpStr)
cmd.PersistentFlags().StringArrayVarP(&args.Set, "set", "s", nil, setFlagHelpStr)
cmd.PersistentFlags().StringVarP(&args.ManifestsPath, "manifests", "d", "", manifestsFlagHelpStr)
}
@@ -87,19 +91,23 @@ func newInstallCmd() *cobra.Command {
# Install higress on local kubernetes cluster
hgctl install --set profile=local-k8s
# Install higress on local docker environment with specific gateway port
hgctl install --set profile=local-docker --set gateway.httpPort=80 --set gateway.httpsPort=443
# To override profile setting
hgctl install --set profile=local-k8s --set global.enableIstioAPI=true --set gateway.replicas=2"
# To override helm setting
hgctl install --set profile=local-k8s --set values.global.proxy.resources.requsts.cpu=500m"
`,
Args: cobra.ExactArgs(0),
PreRunE: func(cmd *cobra.Command, args []string) error {
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return Install(cmd.OutOrStdout(), iArgs)
return install(cmd.OutOrStdout(), iArgs)
},
}
addInstallFlags(installCmd, iArgs)
@@ -108,7 +116,7 @@ func newInstallCmd() *cobra.Command {
return installCmd
}
func Install(writer io.Writer, iArgs *InstallArgs) error {
func install(writer io.Writer, iArgs *InstallArgs) error {
setFlags := applyFlagAliases(iArgs.Set, iArgs.ManifestsPath)
// check profileName
@@ -127,16 +135,22 @@ 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
}
err = InstallManifests(profile, writer)
err = installManifests(profile, writer)
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
}
@@ -158,11 +172,12 @@ func promptInstall(writer io.Writer, profileName string) bool {
func promptProfileName(writer io.Writer) string {
answer := ""
fmt.Fprintf(writer, "Please select higress install configration profile:\n")
fmt.Fprintf(writer, "1.Install higress to local kubernetes cluster like kind etc.\n")
fmt.Fprintf(writer, "2.Install higress to kubernetes cluster\n")
fmt.Fprintf(writer, "\nPlease select higress install configration profile:\n")
fmt.Fprintf(writer, "\n1.Install higress to local kubernetes cluster like kind etc.\n")
fmt.Fprintf(writer, "\n2.Install higress to kubernetes cluster\n")
fmt.Fprintf(writer, "\n3.Install higress to local docker environment\n")
for {
fmt.Fprintf(writer, "Please input 1 or 2 to select, input your selection:")
fmt.Fprintf(writer, "\nPlease input 1, 2 or 3 to select, input your selection:")
fmt.Scanln(&answer)
if strings.TrimSpace(answer) == "1" {
return profileNameLocalK8s
@@ -170,33 +185,23 @@ func promptProfileName(writer io.Writer) string {
if strings.TrimSpace(answer) == "2" {
return profileNameK8s
}
if strings.TrimSpace(answer) == "3" {
return profileNameLocalDocker
}
}
}
func InstallManifests(profile *helm.Profile, writer io.Writer) error {
cliClient, err := kubernetes.NewCLIClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
if err != nil {
return fmt.Errorf("failed to build kubernetes client: %w", err)
}
op, err := installer.NewInstaller(profile, cliClient, writer, false)
if err != nil {
return err
}
if err := op.Run(); err != nil {
return err
}
manifestMap, err := op.RenderManifests()
func installManifests(profile *helm.Profile, writer io.Writer) error {
installer, err := installer.NewInstaller(profile, writer, false)
if err != nil {
return err
}
fmt.Fprintf(writer, "\n⌛ Processing installation... \n\n")
if err := op.ApplyManifests(manifestMap); err != nil {
err = installer.Install()
if err != nil {
return err
}
fmt.Fprintf(writer, "\n🎊 Install All Resources Complete!\n")
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

@@ -0,0 +1,113 @@
// 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 (
"errors"
"fmt"
"io"
"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"
)
const (
GatewayAPI ComponentName = "gatewayAPI"
)
type GatewayAPIComponent struct {
profile *helm.Profile
started bool
opts *ComponentOptions
renderer helm.Renderer
writer io.Writer
kubeCli kubernetes.CLIClient
}
func NewGatewayAPIComponent(kubeCli kubernetes.CLIClient, profile *helm.Profile, writer io.Writer, opts ...ComponentOption) (Component, error) {
newOpts := &ComponentOptions{}
for _, opt := range opts {
opt(newOpts)
}
if !strings.HasPrefix(newOpts.RepoURL, "embed://") {
return nil, errors.New("GatewayAPI Url need start with embed://")
}
chartDir := strings.TrimPrefix(newOpts.RepoURL, "embed://")
// GatewayAPI can only be installed by embed type
renderer, err := helm.NewLocalFileRenderer(
helm.WithName(newOpts.ChartName),
helm.WithNamespace(newOpts.Namespace),
helm.WithRepoURL(newOpts.RepoURL),
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
}
gatewayAPIComponent := &GatewayAPIComponent{
profile: profile,
renderer: renderer,
opts: newOpts,
writer: writer,
kubeCli: kubeCli,
}
return gatewayAPIComponent, nil
}
func (i *GatewayAPIComponent) ComponentName() ComponentName {
return GatewayAPI
}
func (i *GatewayAPIComponent) Namespace() string {
return i.opts.Namespace
}
func (i *GatewayAPIComponent) Enabled() bool {
return true
}
func (i *GatewayAPIComponent) Run() error {
if !i.opts.Quiet {
fmt.Fprintf(i.writer, "🏄 Downloading GatewayAPI Yaml Files version: %s, url: %s\n", i.opts.Version, i.opts.RepoURL)
}
if err := i.renderer.Init(); err != nil {
return err
}
i.started = true
return nil
}
func (i *GatewayAPIComponent) RenderManifest() (string, error) {
if !i.started {
return "", nil
}
if !i.opts.Quiet {
fmt.Fprintf(i.writer, "📦 Rendering GatewayAPI Yaml Files\n")
}
values := make(map[string]any)
manifest, err := renderComponentManifest(values, i.renderer, false, i.ComponentName(), i.opts.Namespace)
if err != nil {
return "", err
}
return manifest, nil
}

View File

@@ -0,0 +1,93 @@
// 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 (
"bytes"
"fmt"
"io"
"os/exec"
"strings"
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
"github.com/alibaba/higress/pkg/cmd/options"
)
type HelmRelease struct {
appVersion string `json:"app_version,omitempty"`
chart string `json:"chart,omitempty"`
name string `json:"name,omitempty"`
namespace string `json:"namespace,omitempty"`
revision string `json:"revision,omitempty"`
status string `json:"status,omitempty"`
updated string `json:"updated,omitempty"`
}
type HelmAgent struct {
profile *helm.Profile
writer io.Writer
helmBinaryName string
quiet bool
}
func NewHelmAgent(profile *helm.Profile, writer io.Writer, quiet bool) *HelmAgent {
return &HelmAgent{
profile: profile,
writer: writer,
helmBinaryName: "helm",
quiet: quiet,
}
}
func (h *HelmAgent) IsHigressInstalled() (bool, error) {
args := []string{"list", "-n", h.profile.Global.Namespace, "-f", "higress"}
if len(*options.DefaultConfigFlags.KubeConfig) > 0 {
args = append(args, fmt.Sprintf("--kubeconfig=%s", *options.DefaultConfigFlags.KubeConfig))
}
if len(*options.DefaultConfigFlags.Context) > 0 {
args = append(args, fmt.Sprintf("--kube-context=%s", *options.DefaultConfigFlags.Context))
}
if !h.quiet {
fmt.Fprintf(h.writer, "\n📦 Running command: %s %s\n\n", h.helmBinaryName, strings.Join(args, " "))
}
cmd := exec.Command(h.helmBinaryName, args...)
var out bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &stderr
if err := cmd.Start(); err != nil {
return false, nil
}
done := make(chan error, 1)
go func() {
done <- cmd.Wait()
}()
select {
case err := <-done:
if err == nil {
content := out.String()
if !h.quiet {
fmt.Fprintf(h.writer, "\n%s\n", content)
}
if strings.Contains(content, "deployed") {
return true, nil
}
}
}
return false, nil
}

View File

@@ -15,11 +15,12 @@
package installer
import (
"errors"
"fmt"
"io"
"os"
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
"github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
)
const (
@@ -32,6 +33,7 @@ type HigressComponent struct {
opts *ComponentOptions
renderer helm.Renderer
writer io.Writer
kubeCli kubernetes.CLIClient
}
func (h *HigressComponent) ComponentName() ComponentName {
@@ -68,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
}
@@ -90,35 +93,27 @@ 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)
}
var renderer helm.Renderer
var err error
if newOpts.RepoURL != "" {
renderer, err = helm.NewRemoteRenderer(
helm.WithName(newOpts.ChartName),
helm.WithNamespace(newOpts.Namespace),
helm.WithRepoURL(newOpts.RepoURL),
helm.WithVersion(newOpts.Version),
)
if err != nil {
return nil, err
}
} else {
renderer, err = helm.NewLocalRenderer(
helm.WithName(newOpts.ChartName),
helm.WithNamespace(newOpts.Namespace),
helm.WithVersion(newOpts.Version),
helm.WithFS(os.DirFS(newOpts.ChartPath)),
helm.WithDir(string(Higress)),
)
if err != nil {
return nil, err
}
if len(newOpts.RepoURL) == 0 {
return nil, errors.New("Higress helm chart url can't be empty")
}
// Higress can only be installed by remote type
renderer, err := helm.NewRemoteRenderer(
helm.WithName(newOpts.ChartName),
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
}
higressComponent := &HigressComponent{
@@ -126,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

@@ -18,196 +18,113 @@ import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
"github.com/alibaba/higress/pkg/cmd/hgctl/helm/object"
"github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
"github.com/alibaba/higress/pkg/cmd/options"
"k8s.io/client-go/util/homedir"
)
type Installer struct {
started bool
components map[ComponentName]Component
kubeCli kubernetes.CLIClient
profile *helm.Profile
writer io.Writer
const (
HgctlHomeDirPath = ".hgctl"
StandaloneInstalledPath = "higress-standalone"
ProfileInstalledPath = "profiles"
InstalledYamlFileName = "install.yaml"
DefaultGatewayAPINamespace = "gateway-system"
DefaultIstioNamespace = "istio-system"
)
type Installer interface {
Install() error
UnInstall() error
Upgrade() error
}
// Run must be invoked before invoking other functions.
func (o *Installer) Run() error {
for name, component := range o.components {
if !component.Enabled() {
continue
}
if err := component.Run(); err != nil {
return fmt.Errorf("component %s run failed, err: %s", name, err)
}
}
o.started = true
return nil
}
// RenderManifests renders component manifests specified by profile.
func (o *Installer) RenderManifests() (map[ComponentName]string, error) {
if !o.started {
return nil, errors.New("HigressOperator is not running")
}
res := make(map[ComponentName]string)
for name, component := range o.components {
if !component.Enabled() {
continue
}
manifest, err := component.RenderManifest()
func NewInstaller(profile *helm.Profile, writer io.Writer, quiet bool) (Installer, error) {
switch profile.Global.Install {
case helm.InstallK8s, helm.InstallLocalK8s:
cliClient, err := kubernetes.NewCLIClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
if err != nil {
return nil, fmt.Errorf("component %s RenderManifest err: %v", name, err)
return nil, fmt.Errorf("failed to build kubernetes client: %w", err)
}
res[name] = manifest
installer, err := NewK8sInstaller(profile, cliClient, writer, quiet)
return installer, err
case helm.InstallLocalDocker:
installer, err := NewDockerInstaller(profile, writer, quiet)
return installer, err
default:
return nil, errors.New("install is not supported")
}
return res, nil
}
// GenerateManifests generates component manifests to k8s cluster
func (o *Installer) GenerateManifests(manifestMap map[ComponentName]string) error {
if o.kubeCli == nil {
return errors.New("no injected k8s cli into Installer")
func GetHomeDir() (string, error) {
home := homedir.HomeDir()
if home == "" {
return "", fmt.Errorf("No user home environment variable found for OS %s", runtime.GOOS)
}
for _, manifest := range manifestMap {
fmt.Fprint(o.writer, manifest)
}
return nil
return home, nil
}
// ApplyManifests apply component manifests to k8s cluster
func (o *Installer) ApplyManifests(manifestMap map[ComponentName]string) error {
if o.kubeCli == nil {
return errors.New("no injected k8s cli into Installer")
}
for name, manifest := range manifestMap {
namespace := o.components[name].Namespace()
if err := o.applyManifest(manifest, namespace); err != nil {
return fmt.Errorf("component %s ApplyManifest err: %v", name, err)
}
}
return nil
}
func (o *Installer) applyManifest(manifest string, ns string) error {
if err := o.kubeCli.CreateNamespace(ns); err != nil {
return err
}
objs, err := object.ParseK8sObjectsFromYAMLManifest(manifest)
func GetHgctlPath() (string, error) {
home, err := GetHomeDir()
if err != nil {
return err
return "", err
}
for _, obj := range objs {
// check namespaced object if namespace property has been existed
if obj.Namespace == "" && o.isNamespacedObject(obj) {
obj.Namespace = ns
obj.UnstructuredObject().SetNamespace(ns)
}
if o.isNamespacedObject(obj) {
fmt.Fprintf(o.writer, "✔️ Installed %s:%s:%s.\n", obj.Kind, obj.Name, obj.Namespace)
} else {
fmt.Fprintf(o.writer, "✔️ Installed %s::%s.\n", obj.Kind, obj.Name)
}
if err := o.kubeCli.ApplyObject(obj.UnstructuredObject()); err != nil {
return err
hgctlPath := filepath.Join(home, HgctlHomeDirPath)
if _, err := os.Stat(hgctlPath); os.IsNotExist(err) {
if err = os.MkdirAll(hgctlPath, os.ModePerm); err != nil {
return "", err
}
}
return nil
return hgctlPath, nil
}
// DeleteManifests delete component manifests to k8s cluster
func (o *Installer) DeleteManifests(manifestMap map[ComponentName]string) error {
if o.kubeCli == nil {
return errors.New("no injected k8s cli into Installer")
}
for name, manifest := range manifestMap {
namespace := o.components[name].Namespace()
if err := o.deleteManifest(manifest, namespace); err != nil {
return fmt.Errorf("component %s DeleteManifest err: %v", name, err)
}
}
return nil
}
// deleteManifest delete manifest to certain namespace
func (o *Installer) deleteManifest(manifest string, ns string) error {
objs, err := object.ParseK8sObjectsFromYAMLManifest(manifest)
func GetDefaultInstallPackagePath() (string, error) {
dir, err := os.Getwd()
if err != nil {
return err
return "", err
}
for _, obj := range objs {
// check namespaced object if namespace property has been existed
if obj.Namespace == "" && o.isNamespacedObject(obj) {
obj.Namespace = ns
obj.UnstructuredObject().SetNamespace(ns)
}
if o.isNamespacedObject(obj) {
fmt.Fprintf(o.writer, "✔️ Removed %s:%s:%s.\n", obj.Kind, obj.Name, obj.Namespace)
} else {
fmt.Fprintf(o.writer, "✔️ Removed %s::%s.\n", obj.Kind, obj.Name)
}
if err := o.kubeCli.DeleteObject(obj.UnstructuredObject()); err != nil {
return err
path := filepath.Join(dir, StandaloneInstalledPath)
if _, err := os.Stat(path); os.IsNotExist(err) {
if err = os.MkdirAll(path, os.ModePerm); err != nil {
return "", err
}
}
return nil
return path, err
}
func (o *Installer) isNamespacedObject(obj *object.K8sObject) bool {
if obj.Kind != "CustomResourceDefinition" && obj.Kind != "ClusterRole" && obj.Kind != "ClusterRoleBinding" {
return true
}
return false
}
func NewInstaller(profile *helm.Profile, cli kubernetes.CLIClient, writer io.Writer, quiet bool) (*Installer, error) {
if profile == nil {
return nil, errors.New("install profile is empty")
}
// initialize components
components := make(map[ComponentName]Component)
opts := []ComponentOption{
WithComponentNamespace(profile.Global.Namespace),
WithComponentChartPath(profile.InstallPackagePath),
WithComponentVersion(profile.Charts.Higress.Version),
WithComponentRepoURL(profile.Charts.Higress.Url),
WithComponentChartName(profile.Charts.Higress.Name),
}
if quiet {
opts = append(opts, WithQuiet())
}
higressComponent, err := NewHigressComponent(profile, writer, opts...)
func GetProfileInstalledPath() (string, error) {
hgctlPath, err := GetHgctlPath()
if err != nil {
return nil, fmt.Errorf("NewHigressComponent failed, err: %s", err)
return "", err
}
components[Higress] = higressComponent
if profile.IstioEnabled() {
opts := []ComponentOption{
WithComponentNamespace(profile.Global.IstioNamespace),
WithComponentChartPath(profile.InstallPackagePath),
WithComponentVersion(profile.Charts.Istio.Version),
WithComponentRepoURL(profile.Charts.Istio.Url),
WithComponentChartName(profile.Charts.Istio.Name),
}
if quiet {
opts = append(opts, WithQuiet())
profilesPath := filepath.Join(hgctlPath, ProfileInstalledPath)
if _, err := os.Stat(profilesPath); os.IsNotExist(err) {
if err = os.MkdirAll(profilesPath, os.ModePerm); err != nil {
return "", err
}
}
istioCRDComponent, err := NewIstioCRDComponent(profile, writer, opts...)
if err != nil {
return nil, fmt.Errorf("NewIstioCRDComponent failed, err: %s", err)
}
components[Istio] = istioCRDComponent
}
op := &Installer{
profile: profile,
components: components,
kubeCli: cli,
writer: writer,
}
return op, nil
return profilesPath, nil
}
func GetInstalledYamlPath() (string, bool) {
profileInstalledPath, err := GetProfileInstalledPath()
if err != nil {
return "", false
}
installedYamlFile := filepath.Join(profileInstalledPath, InstalledYamlFileName)
if _, err := os.Stat(installedYamlFile); os.IsNotExist(err) {
return installedYamlFile, false
}
return installedYamlFile, true
}

View File

@@ -0,0 +1,112 @@
// 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 (
"errors"
"fmt"
"io"
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
)
type DockerInstaller struct {
started bool
standalone *StandaloneComponent
profile *helm.Profile
writer io.Writer
profileStore ProfileStore
}
func (d *DockerInstaller) Install() error {
fmt.Fprintf(d.writer, "\n⌛ Processing installation... \n\n")
if err := d.standalone.Install(); 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
}
func (d *DockerInstaller) UnInstall() error {
fmt.Fprintf(d.writer, "\n⌛ Processing uninstallation... \n\n")
if err := d.standalone.UnInstall(); err != nil {
return err
}
profileName, err1 := d.profileStore.Delete(d.profile)
if err1 != nil {
return err1
}
fmt.Fprintf(d.writer, "\n✔ Removed Profile: \"%s\" \n", profileName)
fmt.Fprintf(d.writer, "\n🎊 Uninstall All Resources Complete!\n")
return nil
}
func (d *DockerInstaller) Upgrade() error {
fmt.Fprintf(d.writer, "\n⌛ Processing upgrade... \n\n")
if err := d.standalone.Upgrade(); err != nil {
return err
}
fmt.Fprintf(d.writer, "\n🎊 Install All Resources Complete!\n")
return nil
}
func NewDockerInstaller(profile *helm.Profile, writer io.Writer, quiet bool) (*DockerInstaller, error) {
if profile == nil {
return nil, errors.New("install profile is empty")
}
// initialize components
opts := []ComponentOption{
WithComponentVersion(profile.Charts.Standalone.Version),
WithComponentRepoURL(profile.Charts.Standalone.Url),
WithComponentChartName(profile.Charts.Standalone.Name),
}
if quiet {
opts = append(opts, WithQuiet())
}
standaloneComponent, err := NewStandaloneComponent(profile, writer, opts...)
if err != nil {
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,
profileStore: profileStore,
}
return op, nil
}

View File

@@ -0,0 +1,343 @@
// 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 (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
"github.com/alibaba/higress/pkg/cmd/hgctl/helm/object"
"github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
"github.com/alibaba/higress/pkg/cmd/hgctl/util"
)
type K8sInstaller struct {
started bool
components map[ComponentName]Component
kubeCli kubernetes.CLIClient
profile *helm.Profile
writer io.Writer
profileStore ProfileStore
}
func (o *K8sInstaller) Install() error {
// check if higress is installed by helm
fmt.Fprintf(o.writer, "\n⌛ Detecting higress installed by helm or not... \n\n")
helmAgent := NewHelmAgent(o.profile, o.writer, false)
if helmInstalled, _ := helmAgent.IsHigressInstalled(); helmInstalled {
fmt.Fprintf(o.writer, "\n🧐 You have already installed higress by helm, please use \"helm upgrade\" to upgrade higress!\n")
return nil
}
if err := o.Run(); err != nil {
return err
}
manifestMap, err := o.RenderManifests()
if err != nil {
return err
}
fmt.Fprintf(o.writer, "\n⌛ Processing installation... \n\n")
if err := o.ApplyManifests(manifestMap); 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")
return nil
}
func (o *K8sInstaller) UnInstall() error {
if _, err := GetProfileInstalledPath(); err != nil {
return err
}
if err := o.Run(); err != nil {
return err
}
manifestMap, err := o.RenderManifests()
if err != nil {
return err
}
fmt.Fprintf(o.writer, "\n⌛ Processing uninstallation... \n\n")
if err := o.DeleteManifests(manifestMap); err != nil {
return err
}
profileName, err1 := o.profileStore.Delete(o.profile)
if err1 != nil {
return err1
}
fmt.Fprintf(o.writer, "\n✔ Removed Profile: \"%s\" \n", profileName)
fmt.Fprintf(o.writer, "\n🎊 Uninstall All Resources Complete!\n")
return nil
}
func (o *K8sInstaller) Upgrade() error {
return o.Install()
}
// Run must be invoked before invoking other functions.
func (o *K8sInstaller) Run() error {
for name, component := range o.components {
if !component.Enabled() {
continue
}
if err := component.Run(); err != nil {
return fmt.Errorf("component %s run failed, err: %s", name, err)
}
}
o.started = true
return nil
}
// RenderManifests renders component manifests specified by profile.
func (o *K8sInstaller) RenderManifests() (map[ComponentName]string, error) {
if !o.started {
return nil, errors.New("higress installer is not running")
}
res := make(map[ComponentName]string)
for name, component := range o.components {
if !component.Enabled() {
continue
}
manifest, err := component.RenderManifest()
if err != nil {
return nil, fmt.Errorf("component %s RenderManifest err: %v", name, err)
}
res[name] = manifest
}
return res, nil
}
// GenerateManifests generates component manifests to k8s cluster
func (o *K8sInstaller) GenerateManifests(manifestMap map[ComponentName]string) error {
if o.kubeCli == nil {
return errors.New("no injected k8s cli into K8sInstaller")
}
for _, manifest := range manifestMap {
fmt.Fprint(o.writer, manifest)
}
return nil
}
// ApplyManifests apply component manifests to k8s cluster
func (o *K8sInstaller) ApplyManifests(manifestMap map[ComponentName]string) error {
if o.kubeCli == nil {
return errors.New("no injected k8s cli into K8sInstaller")
}
for name, manifest := range manifestMap {
namespace := o.components[name].Namespace()
if err := o.applyManifest(manifest, namespace); err != nil {
return fmt.Errorf("component %s ApplyManifest err: %v", name, err)
}
}
return nil
}
func (o *K8sInstaller) applyManifest(manifest string, ns string) error {
if err := o.kubeCli.CreateNamespace(ns); err != nil {
return err
}
objs, err := object.ParseK8sObjectsFromYAMLManifest(manifest)
if err != nil {
return err
}
for _, obj := range objs {
// check namespaced object if namespace property has been existed
if obj.Namespace == "" && o.isNamespacedObject(obj) {
obj.Namespace = ns
obj.UnstructuredObject().SetNamespace(ns)
}
if o.isNamespacedObject(obj) {
fmt.Fprintf(o.writer, "✔️ Installed %s:%s:%s.\n", obj.Kind, obj.Name, obj.Namespace)
} else {
fmt.Fprintf(o.writer, "✔️ Installed %s::%s.\n", obj.Kind, obj.Name)
}
if err := o.kubeCli.ApplyObject(obj.UnstructuredObject()); err != nil {
return err
}
}
return nil
}
// DeleteManifests delete component manifests to k8s cluster
func (o *K8sInstaller) DeleteManifests(manifestMap map[ComponentName]string) error {
if o.kubeCli == nil {
return errors.New("no injected k8s cli into K8sInstaller")
}
for name, manifest := range manifestMap {
namespace := o.components[name].Namespace()
if err := o.deleteManifest(manifest, namespace); err != nil {
return fmt.Errorf("component %s DeleteManifest err: %v", name, 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)
if err != nil {
return err
}
for _, obj := range objs {
// check namespaced object if namespace property has been existed
if obj.Namespace == "" && o.isNamespacedObject(obj) {
obj.Namespace = ns
obj.UnstructuredObject().SetNamespace(ns)
}
if o.isNamespacedObject(obj) {
fmt.Fprintf(o.writer, "✔️ Removed %s:%s:%s.\n", obj.Kind, obj.Name, obj.Namespace)
} else {
fmt.Fprintf(o.writer, "✔️ Removed %s::%s.\n", obj.Kind, obj.Name)
}
if err := o.kubeCli.DeleteObject(obj.UnstructuredObject()); err != nil {
return err
}
}
return nil
}
func (o *K8sInstaller) isNamespacedObject(obj *object.K8sObject) bool {
if obj.Kind != "CustomResourceDefinition" && obj.Kind != "ClusterRole" && obj.Kind != "ClusterRoleBinding" {
return true
}
return false
}
func NewK8sInstaller(profile *helm.Profile, cli kubernetes.CLIClient, writer io.Writer, quiet bool) (*K8sInstaller, error) {
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{
WithComponentNamespace(profile.Global.Namespace),
WithComponentChartPath(profile.InstallPackagePath),
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(cli, profile, writer, opts...)
if err != nil {
return nil, fmt.Errorf("NewHigressComponent failed, err: %s", err)
}
components[Higress] = higressComponent
if profile.IstioEnabled() {
istioNamespace := profile.GetIstioNamespace()
if len(istioNamespace) == 0 {
istioNamespace = DefaultIstioNamespace
}
opts := []ComponentOption{
WithComponentNamespace(istioNamespace),
WithComponentVersion("1.18.2"),
WithComponentRepoURL("embed://istiobase"),
WithComponentChartName("istio"),
WithComponentCapabilities(capabilities),
}
if quiet {
opts = append(opts, WithQuiet())
}
istioCRDComponent, err := NewIstioCRDComponent(cli, profile, writer, opts...)
if err != nil {
return nil, fmt.Errorf("NewIstioCRDComponent failed, err: %s", err)
}
components[Istio] = istioCRDComponent
}
if profile.GatewayAPIEnabled() {
opts := []ComponentOption{
WithComponentNamespace(DefaultGatewayAPINamespace),
WithComponentVersion("1.0.0"),
WithComponentRepoURL("embed://gatewayapi"),
WithComponentChartName("gatewayAPI"),
WithComponentCapabilities(capabilities),
}
if quiet {
opts = append(opts, WithQuiet())
}
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,
profileStore: profileStore,
}
return op, nil
}

View File

@@ -17,9 +17,11 @@ package installer
import (
"fmt"
"io"
"os"
"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"
)
const (
@@ -32,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)
@@ -42,23 +45,31 @@ func NewIstioCRDComponent(profile *helm.Profile, writer io.Writer, opts ...Compo
var renderer helm.Renderer
var err error
if newOpts.RepoURL != "" {
renderer, err = helm.NewRemoteRenderer(
// Istio can be installed by embed type or remote type
if strings.HasPrefix(newOpts.RepoURL, "embed://") {
chartDir := strings.TrimPrefix(newOpts.RepoURL, "embed://")
renderer, err = helm.NewLocalChartRenderer(
helm.WithName(newOpts.ChartName),
helm.WithNamespace(newOpts.Namespace),
helm.WithRepoURL(newOpts.RepoURL),
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
}
} else {
renderer, err = helm.NewLocalRenderer(
renderer, err = helm.NewRemoteRenderer(
helm.WithName(newOpts.ChartName),
helm.WithNamespace(newOpts.Namespace),
helm.WithRepoURL(newOpts.RepoURL),
helm.WithVersion(newOpts.Version),
helm.WithFS(os.DirFS(newOpts.ChartPath)),
helm.WithDir(string(Istio)),
helm.WithCapabilities(newOpts.Capabilities),
helm.WithRestConfig(kubeCli.RESTConfig()),
)
if err != nil {
return nil, err
@@ -70,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

@@ -0,0 +1,141 @@
// 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"
"fmt"
"io"
"os"
"strings"
"time"
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
"github.com/alibaba/higress/pkg/cmd/hgctl/util"
)
const (
defaultHttpRequestTimeout = 15 * time.Second
defaultHttpMaxTry = 3
defaultHttpBufferSize = 1024 * 1024 * 2
)
type StandaloneComponent struct {
profile *helm.Profile
started bool
opts *ComponentOptions
writer io.Writer
httpFetcher *util.HTTPFetcher
agent *Agent
}
func (s *StandaloneComponent) Install() error {
if !s.opts.Quiet {
fmt.Fprintf(s.writer, "\n🏄 Downloading installer from %s\n", s.opts.RepoURL)
}
// download get-higress.sh
data, err := s.httpFetcher.Fetch(context.Background(), s.opts.RepoURL)
if err != nil {
return err
}
// write installer binary shell
if err := util.WriteFileString(s.agent.installBinaryName, string(data), os.ModePerm); err != nil {
return err
}
// start to install higress
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
}
func (s *StandaloneComponent) UnInstall() error {
if err := s.agent.Uninstall(); err != nil {
return err
}
return nil
}
func (s *StandaloneComponent) Upgrade() error {
if !s.opts.Quiet {
fmt.Fprintf(s.writer, "\n🏄 Downloading installer from %s\n", s.opts.RepoURL)
}
// download get-higress.sh
data, err := s.httpFetcher.Fetch(context.Background(), s.opts.RepoURL)
if err != nil {
return err
}
// write installer binary shell
if err := util.WriteFileString(s.agent.installBinaryName, string(data), os.ModePerm); err != nil {
return err
}
// start to upgrade higress
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
}
func NewStandaloneComponent(profile *helm.Profile, writer io.Writer, opts ...ComponentOption) (*StandaloneComponent, error) {
newOpts := &ComponentOptions{}
for _, opt := range opts {
opt(newOpts)
}
httpFetcher := util.NewHTTPFetcher(defaultHttpRequestTimeout, defaultHttpMaxTry, defaultHttpBufferSize)
if err := prepareProfile(profile); err != nil {
return nil, err
}
agent := NewAgent(profile, writer, newOpts.Quiet)
standaloneComponent := &StandaloneComponent{
profile: profile,
opts: newOpts,
writer: writer,
httpFetcher: httpFetcher,
agent: agent,
}
return standaloneComponent, nil
}
func prepareProfile(profile *helm.Profile) error {
if len(profile.InstallPackagePath) == 0 {
dir, err := GetDefaultInstallPackagePath()
if err != nil {
return err
}
profile.InstallPackagePath = dir
}
if _, err := os.Stat(profile.InstallPackagePath); os.IsNotExist(err) {
if err = os.MkdirAll(profile.InstallPackagePath, os.ModePerm); err != nil {
return err
}
}
// parse INSTALLPACKAGEPATH in storage.url
if strings.HasPrefix(profile.Storage.Url, "file://") {
profile.Storage.Url = strings.ReplaceAll(profile.Storage.Url, "${INSTALLPACKAGEPATH}", profile.InstallPackagePath)
}
return nil
}

View File

@@ -0,0 +1,356 @@
// 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 (
"bytes"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
)
type RunSudoState string
const (
NoSudo RunSudoState = "NoSudo"
SudoWithoutPassword RunSudoState = "SudoWithoutPassword"
SudoWithPassword RunSudoState = "SudoWithPassword"
)
type Agent struct {
profile *helm.Profile
writer io.Writer
shutdownBinaryName string
resetBinaryName string
startupBinaryName string
installBinaryName string
installPath string
configuredPath string
higressPath string
versionPath string
quiet bool
runSudoState RunSudoState
}
func NewAgent(profile *helm.Profile, writer io.Writer, quiet bool) *Agent {
installPath := profile.InstallPackagePath
return &Agent{
profile: profile,
writer: writer,
installPath: installPath,
higressPath: filepath.Join(installPath, "higress"),
installBinaryName: filepath.Join(installPath, "get-higress.sh"),
shutdownBinaryName: filepath.Join(installPath, "higress", "bin", "shutdown.sh"),
resetBinaryName: filepath.Join(installPath, "higress", "bin", "reset.sh"),
startupBinaryName: filepath.Join(installPath, "higress", "bin", "startup.sh"),
configuredPath: filepath.Join(installPath, "higress", "compose", ".configured"),
versionPath: filepath.Join(installPath, "higress", "VERSION"),
quiet: quiet,
runSudoState: NoSudo,
}
}
func (a *Agent) profileArgs() []string {
args := []string{
fmt.Sprintf("--nacos-ns=%s", a.profile.Storage.Ns),
fmt.Sprintf("--config-url=%s", a.profile.Storage.Url),
fmt.Sprintf("--nacos-ns=%s", a.profile.Storage.Ns),
fmt.Sprintf("--nacos-password=%s", a.profile.Storage.Password),
fmt.Sprintf("--nacos-username=%s", a.profile.Storage.Username),
fmt.Sprintf("--data-enc-key=%s", a.profile.Storage.DataEncKey),
fmt.Sprintf("--console-port=%d", a.profile.Console.Port),
fmt.Sprintf("--gateway-http-port=%d", a.profile.Gateway.HttpPort),
fmt.Sprintf("--gateway-https-port=%d", a.profile.Gateway.HttpsPort),
fmt.Sprintf("--gateway-metrics-port=%d", a.profile.Gateway.MetricsPort),
}
return args
}
func (a *Agent) run(binaryName string, args []string, autoSudo bool) error {
var cmd *exec.Cmd
if !autoSudo || a.runSudoState == NoSudo {
if !a.quiet {
fmt.Fprintf(a.writer, "\n📦 Running command: %s %s\n\n", binaryName, strings.Join(args, " "))
}
cmd = exec.Command(binaryName, args...)
} else {
newArgs := make([]string, 0)
newArgs = append(newArgs, binaryName)
newArgs = append(newArgs, args...)
if !a.quiet {
fmt.Fprintf(a.writer, "\n📦 Running command: %s %s\n\n", "sudo", strings.Join(newArgs, " "))
}
cmd = exec.Command("sudo", newArgs...)
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Dir = a.installPath
if err := cmd.Start(); err != nil {
return err
}
done := make(chan error, 1)
go func() {
done <- cmd.Wait()
}()
select {
case err := <-done:
return err
}
return nil
}
func (a *Agent) checkSudoPermission() error {
if !a.quiet {
fmt.Fprintf(a.writer, "\n⌛ Checking docker command sudo permission... ")
}
// check docker ps command
cmd := exec.Command("docker", "ps")
var out bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &stderr
cmd.Dir = a.installPath
if err := cmd.Start(); err != nil {
return err
}
done := make(chan error, 1)
go func() {
done <- cmd.Wait()
}()
select {
case err := <-done:
if err == nil {
if !a.quiet {
fmt.Fprintf(a.writer, "checked result: no need sudo permission\n")
}
a.runSudoState = NoSudo
return nil
}
}
// check sudo docker ps command
cmd2 := exec.Command("sudo", "-S", "docker", "ps")
var out2 bytes.Buffer
var stderr2 bytes.Buffer
cmd2.Stdout = &out2
cmd2.Stderr = &stderr2
cmd2.Dir = a.installPath
stdin, _ := cmd2.StdinPipe()
defer stdin.Close()
if err := cmd2.Start(); err != nil {
return err
}
done2 := make(chan error, 1)
go func() {
done2 <- cmd2.Wait()
}()
select {
case <-time.After(5 * time.Second):
cmd2.Process.Signal(os.Interrupt)
if !a.quiet {
fmt.Fprintf(a.writer, "checked result: timeout execeed and need sudo with password\n")
}
a.runSudoState = SudoWithPassword
case err := <-done2:
if err == nil {
if !a.quiet {
fmt.Fprintf(a.writer, "checked result: need sudo without password\n")
}
a.runSudoState = SudoWithoutPassword
} else {
if !a.quiet {
fmt.Fprintf(a.writer, "checked result: need sudo with password\n")
}
a.runSudoState = SudoWithPassword
}
}
return nil
}
func (a *Agent) Install() error {
a.checkSudoPermission()
if a.runSudoState == SudoWithPassword {
if !a.promptSudo() {
return errors.New("cancel installation")
}
}
if a.hasConfigured() {
a.Reset()
}
if !a.quiet {
fmt.Fprintf(a.writer, "\n⌛ Starting to install higress.. \n")
}
args := []string{"./higress"}
args = append(args, a.profileArgs()...)
return a.run(a.installBinaryName, args, true)
return nil
}
func (a *Agent) Uninstall() error {
a.checkSudoPermission()
if a.runSudoState == SudoWithPassword {
if !a.promptSudo() {
return errors.New("cancel uninstall")
}
}
if !a.quiet {
fmt.Fprintf(a.writer, "\n⌛ Starting to uninstall higress... \n")
}
if err := a.Reset(); err != nil {
return err
}
return nil
}
func (a *Agent) Upgrade() error {
a.checkSudoPermission()
if a.runSudoState == SudoWithPassword {
if !a.promptSudo() {
return errors.New("cancel upgrade")
}
}
currentVersion := ""
newVersion := ""
if !a.quiet {
fmt.Fprintf(a.writer, "\n⌛ Checking current higress version... ")
currentVersion, _ = a.Version()
fmt.Fprintf(a.writer, "%s\n", currentVersion)
}
if !a.quiet {
fmt.Fprintf(a.writer, "\n⌛ Starting to upgrade higress... \n")
}
if err := a.run(a.installBinaryName, []string{"-u"}, true); err != nil {
return err
}
if !a.quiet {
fmt.Fprintf(a.writer, "\n⌛ Checking new higress version... ")
newVersion, _ = a.Version()
fmt.Fprintf(a.writer, "%s\n", newVersion)
}
if currentVersion == newVersion {
return nil
}
if !a.promptRestart() {
return nil
}
if err := a.Shutdown(); err != nil {
return err
}
if err := a.Startup(); err != nil {
return err
}
return nil
}
func (a *Agent) Version() (string, error) {
version := ""
content, err := os.ReadFile(a.versionPath)
if err != nil {
return version, nil
}
return string(content), nil
}
func (a *Agent) promptSudo() bool {
answer := ""
for {
fmt.Fprintf(a.writer, "\nThis need sudo permission and input root password to continue installation, Proceed? (y/N)")
fmt.Scanln(&answer)
if strings.TrimSpace(answer) == "y" {
fmt.Fprintf(a.writer, "\n")
return true
}
if strings.TrimSpace(answer) == "N" {
fmt.Fprintf(a.writer, "Cancelled.\n")
return false
}
}
}
func (a *Agent) promptRestart() bool {
answer := ""
for {
fmt.Fprintf(a.writer, "\nThis need to restart higress, Proceed? (y/N)")
fmt.Scanln(&answer)
if strings.TrimSpace(answer) == "y" {
fmt.Fprintf(a.writer, "\n")
return true
}
if strings.TrimSpace(answer) == "N" {
fmt.Fprintf(a.writer, "Cancelled.\n")
return false
}
}
}
func (a *Agent) Startup() error {
if !a.quiet {
fmt.Fprintf(a.writer, "\n⌛ Starting higress... \n")
}
return a.run(a.startupBinaryName, []string{}, true)
}
func (a *Agent) Shutdown() error {
if !a.quiet {
fmt.Fprintf(a.writer, "\n⌛ Shutdowning higress... \n")
}
return a.run(a.shutdownBinaryName, []string{}, true)
}
func (a *Agent) Reset() error {
if !a.quiet {
fmt.Fprintf(a.writer, "\n⌛ Resetting higress....\n")
}
return a.run(a.resetBinaryName, []string{}, true)
}
func (a *Agent) hasConfigured() bool {
if _, err := os.Stat(a.configuredPath); os.IsNotExist(err) {
return false
}
return true
}

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

@@ -0,0 +1,130 @@
// 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 kubernetes
import (
"context"
"github.com/spf13/pflag"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
const (
DefaultHigressNamespace = "higress-system"
HigressExtGroup = "extensions.higress.io"
HigressExtVersion = "v1alpha1"
HigressExtAPIVersion = HigressExtGroup + "/" + HigressExtVersion
WasmPluginKind = "WasmPlugin"
WasmPluginResource = "wasmplugins"
)
var (
HigressNamespace = DefaultHigressNamespace
WasmPluginGVK = schema.GroupVersionKind{Group: HigressExtGroup, Version: HigressExtVersion, Kind: WasmPluginKind}
WasmPluginGVR = schema.GroupVersionResource{Group: HigressExtGroup, Version: HigressExtVersion, Resource: WasmPluginResource}
)
func AddHigressNamespaceFlags(flags *pflag.FlagSet) {
flags.StringVarP(&HigressNamespace, "namespace", "n",
DefaultHigressNamespace, "Namespace where Higress was installed")
}
type WasmPluginClient struct {
dyn *DynamicClient
}
func NewWasmPluginClient(dynClient *DynamicClient) *WasmPluginClient {
return &WasmPluginClient{dynClient}
}
func (c WasmPluginClient) Get(ctx context.Context, name string) (*unstructured.Unstructured, error) {
return c.dyn.Get(ctx, WasmPluginGVR, HigressNamespace, name)
}
func (c WasmPluginClient) List(ctx context.Context) (*unstructured.UnstructuredList, error) {
return c.dyn.List(ctx, WasmPluginGVR, HigressNamespace)
}
func (c WasmPluginClient) Create(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
return c.dyn.Create(ctx, WasmPluginGVR, HigressNamespace, obj)
}
func (c WasmPluginClient) Delete(ctx context.Context, name string) (*unstructured.Unstructured, error) {
return c.dyn.Delete(ctx, WasmPluginGVR, HigressNamespace, name)
}
func (c WasmPluginClient) Update(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
return c.dyn.Update(ctx, WasmPluginGVR, HigressNamespace, obj)
}
// TODO(WeixinX): Will be changed to WasmPlugin specific Client instead of Unstructured
type DynamicClient struct {
config *rest.Config
client dynamic.Interface
}
func NewDynamicClient(clientConfig clientcmd.ClientConfig) (*DynamicClient, error) {
var (
c DynamicClient
err error
)
c.config, err = clientConfig.ClientConfig()
if err != nil {
return nil, err
}
c.client, err = dynamic.NewForConfig(c.config)
if err != nil {
return nil, err
}
return &c, nil
}
func (c DynamicClient) Get(ctx context.Context, gvr schema.GroupVersionResource, namespace, name string) (*unstructured.Unstructured, error) {
return c.client.Resource(gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
}
func (c DynamicClient) List(ctx context.Context, gvr schema.GroupVersionResource, namespace string) (*unstructured.UnstructuredList, error) {
return c.client.Resource(gvr).Namespace(namespace).List(ctx, metav1.ListOptions{})
}
func (c DynamicClient) Create(ctx context.Context, gvr schema.GroupVersionResource, namespace string, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
return c.client.Resource(gvr).Namespace(namespace).Create(ctx, obj, metav1.CreateOptions{})
}
func (c DynamicClient) Delete(ctx context.Context, gvr schema.GroupVersionResource, namespace, name string) (*unstructured.Unstructured, error) {
result, err := c.client.Resource(gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return nil, err
}
err = c.client.Resource(gvr).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{})
if err != nil {
return nil, err
}
return result, nil
}
func (c DynamicClient) Update(ctx context.Context, gvr schema.GroupVersionResource, namespace string,
obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
return c.client.Resource(gvr).Namespace(namespace).Update(ctx, obj, metav1.UpdateOptions{})
}

View File

@@ -59,12 +59,15 @@ func newManifestCmd() *cobra.Command {
generate := newManifestGenerateCmd(iArgs)
addManifestFlags(generate, iArgs)
flags := generate.Flags()
options.AddKubeConfigFlags(flags)
manifestCmd.AddCommand(generate)
return manifestCmd
}
func addManifestFlags(cmd *cobra.Command, args *ManifestArgs) {
cmd.PersistentFlags().StringSliceVarP(&args.InFilenames, "filename", "f", nil, filenameFlagHelpStr)
cmd.PersistentFlags().StringArrayVarP(&args.Set, "set", "s", nil, setFlagHelpStr)
cmd.PersistentFlags().StringVarP(&args.ManifestsPath, "manifests", "d", "", manifestsFlagHelpStr)
}
@@ -123,7 +126,7 @@ func genManifests(profile *helm.Profile, writer io.Writer) error {
return fmt.Errorf("failed to build kubernetes client: %w", err)
}
op, err := installer.NewInstaller(profile, cliClient, writer, true)
op, err := installer.NewK8sInstaller(profile, cliClient, writer, true)
if err != nil {
return err
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
apiVersion: v1
appVersion: 1.18.2
description: Helm chart for deploying Istio cluster resources and CRDs
icon: https://istio.io/latest/favicons/android-192x192.png
keywords:
- istio
name: base
sources:
- https://github.com/istio/istio
version: 1.18.2

View File

@@ -0,0 +1,21 @@
# Istio base Helm Chart
This chart installs resources shared by all Istio revisions. This includes Istio CRDs.
## Setup Repo Info
```console
helm repo add istio https://istio-release.storage.googleapis.com/charts
helm repo update
```
_See [helm repo](https://helm.sh/docs/helm/helm_repo/) for command documentation._
## Installing the Chart
To install the chart with the release name `istio-base`:
```console
kubectl create namespace istio-system
helm install istio-base istio/base -n istio-system
```

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,48 @@
# SYNC WITH manifests/charts/istio-operator/templates
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: istiooperators.install.istio.io
labels:
release: istio
spec:
conversion:
strategy: None
group: install.istio.io
names:
kind: IstioOperator
listKind: IstioOperatorList
plural: istiooperators
singular: istiooperator
shortNames:
- iop
- io
scope: Namespaced
versions:
- additionalPrinterColumns:
- description: Istio control plane revision
jsonPath: .spec.revision
name: Revision
type: string
- description: IOP current state
jsonPath: .status.status
name: Status
type: string
- description: 'CreationTimestamp is a timestamp representing the server time
when this object was created. It is not guaranteed to be set in happens-before
order across separate operations. Clients may not set this value. It is represented
in RFC3339 form and is in UTC. Populated by the system. Read-only. Null for
lists. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata'
jsonPath: .metadata.creationTimestamp
name: Age
type: date
subresources:
status: {}
name: v1alpha1
schema:
openAPIV3Schema:
type: object
x-kubernetes-preserve-unknown-fields: true
served: true
storage: true
---

View File

@@ -0,0 +1,5 @@
Istio base successfully installed!
To learn more about the release, try:
$ helm status {{ .Release.Name }}
$ helm get all {{ .Release.Name }}

View File

@@ -0,0 +1,181 @@
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# DO NOT EDIT!
# THIS IS A LEGACY CHART HERE FOR BACKCOMPAT
# UPDATED CHART AT manifests/charts/istio-control/istio-discovery
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: istiod-{{ .Values.global.istioNamespace }}
labels:
app: istiod
release: {{ .Release.Name }}
rules:
# sidecar injection controller
- apiGroups: ["admissionregistration.k8s.io"]
resources: ["mutatingwebhookconfigurations"]
verbs: ["get", "list", "watch", "update", "patch"]
# configuration validation webhook controller
- apiGroups: ["admissionregistration.k8s.io"]
resources: ["validatingwebhookconfigurations"]
verbs: ["get", "list", "watch", "update"]
# istio configuration
# removing CRD permissions can break older versions of Istio running alongside this control plane (https://github.com/istio/istio/issues/29382)
# please proceed with caution
- apiGroups: ["config.istio.io", "security.istio.io", "networking.istio.io", "authentication.istio.io", "rbac.istio.io", "telemetry.istio.io"]
verbs: ["get", "watch", "list"]
resources: ["*"]
{{- if .Values.global.istiod.enableAnalysis }}
- apiGroups: ["config.istio.io", "security.istio.io", "networking.istio.io", "authentication.istio.io", "rbac.istio.io", "telemetry.istio.io"]
verbs: ["update"]
# TODO: should be on just */status but wildcard is not supported
resources: ["*"]
{{- end }}
- apiGroups: ["networking.istio.io"]
verbs: [ "get", "watch", "list", "update", "patch", "create", "delete" ]
resources: [ "workloadentries" ]
- apiGroups: ["networking.istio.io"]
verbs: [ "get", "watch", "list", "update", "patch", "create", "delete" ]
resources: [ "workloadentries/status" ]
# auto-detect installed CRD definitions
- apiGroups: ["apiextensions.k8s.io"]
resources: ["customresourcedefinitions"]
verbs: ["get", "list", "watch"]
# discovery and routing
- apiGroups: [""]
resources: ["pods", "nodes", "services", "namespaces", "endpoints"]
verbs: ["get", "list", "watch"]
- apiGroups: ["discovery.k8s.io"]
resources: ["endpointslices"]
verbs: ["get", "list", "watch"]
# ingress controller
{{- if .Values.global.istiod.enableAnalysis }}
- apiGroups: ["extensions", "networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get", "list", "watch"]
- apiGroups: ["extensions", "networking.k8s.io"]
resources: ["ingresses/status"]
verbs: ["*"]
{{- end}}
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses", "ingressclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses/status"]
verbs: ["*"]
# required for CA's namespace controller
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["create", "get", "list", "watch", "update"]
# Istiod and bootstrap.
- apiGroups: ["certificates.k8s.io"]
resources:
- "certificatesigningrequests"
- "certificatesigningrequests/approval"
- "certificatesigningrequests/status"
verbs: ["update", "create", "get", "delete", "watch"]
- apiGroups: ["certificates.k8s.io"]
resources:
- "signers"
resourceNames:
- "kubernetes.io/legacy-unknown"
verbs: ["approve"]
# Used by Istiod to verify the JWT tokens
- apiGroups: ["authentication.k8s.io"]
resources: ["tokenreviews"]
verbs: ["create"]
# Used by Istiod to verify gateway SDS
- apiGroups: ["authorization.k8s.io"]
resources: ["subjectaccessreviews"]
verbs: ["create"]
# Use for Kubernetes Service APIs
- apiGroups: ["networking.x-k8s.io", "gateway.networking.k8s.io"]
resources: ["*"]
verbs: ["get", "watch", "list"]
- apiGroups: ["networking.x-k8s.io", "gateway.networking.k8s.io"]
resources: ["*"] # TODO: should be on just */status but wildcard is not supported
verbs: ["update"]
- apiGroups: ["gateway.networking.k8s.io"]
resources: ["gatewayclasses"]
verbs: ["create", "update", "patch", "delete"]
# Needed for multicluster secret reading, possibly ingress certs in the future
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "watch", "list"]
# Used for MCS serviceexport management
- apiGroups: ["multicluster.x-k8s.io"]
resources: ["serviceexports"]
verbs: ["get", "watch", "list", "create", "delete"]
# Used for MCS serviceimport management
- apiGroups: ["multicluster.x-k8s.io"]
resources: ["serviceimports"]
verbs: ["get", "watch", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: istio-reader-{{ .Values.global.istioNamespace }}
labels:
app: istio-reader
release: {{ .Release.Name }}
rules:
- apiGroups:
- "config.istio.io"
- "security.istio.io"
- "networking.istio.io"
- "authentication.istio.io"
- "rbac.istio.io"
resources: ["*"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["endpoints", "pods", "services", "nodes", "replicationcontrollers", "namespaces", "secrets"]
verbs: ["get", "list", "watch"]
- apiGroups: ["networking.istio.io"]
verbs: [ "get", "watch", "list" ]
resources: [ "workloadentries" ]
- apiGroups: ["apiextensions.k8s.io"]
resources: ["customresourcedefinitions"]
verbs: ["get", "list", "watch"]
- apiGroups: ["discovery.k8s.io"]
resources: ["endpointslices"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["replicasets"]
verbs: ["get", "list", "watch"]
- apiGroups: ["authentication.k8s.io"]
resources: ["tokenreviews"]
verbs: ["create"]
- apiGroups: ["authorization.k8s.io"]
resources: ["subjectaccessreviews"]
verbs: ["create"]
- apiGroups: ["multicluster.x-k8s.io"]
resources: ["serviceexports"]
verbs: ["get", "watch", "list"]
- apiGroups: ["multicluster.x-k8s.io"]
resources: ["serviceimports"]
verbs: ["get", "watch", "list"]
{{- if or .Values.global.externalIstiod }}
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["create", "get", "list", "watch", "update"]
- apiGroups: ["admissionregistration.k8s.io"]
resources: ["mutatingwebhookconfigurations"]
verbs: ["get", "list", "watch", "update", "patch"]
- apiGroups: ["admissionregistration.k8s.io"]
resources: ["validatingwebhookconfigurations"]
verbs: ["get", "list", "watch", "update"]
{{- end}}
---

View File

@@ -0,0 +1,37 @@
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# DO NOT EDIT!
# THIS IS A LEGACY CHART HERE FOR BACKCOMPAT
# UPDATED CHART AT manifests/charts/istio-control/istio-discovery
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: istio-reader-{{ .Values.global.istioNamespace }}
labels:
app: istio-reader
release: {{ .Release.Name }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: istio-reader-{{ .Values.global.istioNamespace }}
subjects:
- kind: ServiceAccount
name: istio-reader-service-account
namespace: {{ .Values.global.istioNamespace }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: istiod-{{ .Values.global.istioNamespace }}
labels:
app: istiod
release: {{ .Release.Name }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: istiod-{{ .Values.global.istioNamespace }}
subjects:
- kind: ServiceAccount
name: istiod-service-account
namespace: {{ .Values.global.istioNamespace }}
---

View File

@@ -0,0 +1,4 @@
{{- if .Values.base.enableCRDTemplates }}
{{ .Files.Get "crds/crd-all.gen.yaml" }}
{{ .Files.Get "crds/crd-operator.yaml" }}
{{- end }}

View File

@@ -0,0 +1,48 @@
{{- if not (eq .Values.defaultRevision "") }}
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: istiod-default-validator
labels:
app: istiod
release: {{ .Release.Name }}
istio: istiod
istio.io/rev: {{ .Values.defaultRevision }}
webhooks:
- name: validation.istio.io
clientConfig:
{{- if .Values.base.validationURL }}
url: {{ .Values.base.validationURL }}
{{- else }}
service:
{{- if (eq .Values.defaultRevision "default") }}
name: istiod
{{- else }}
name: istiod-{{ .Values.defaultRevision }}
{{- end }}
namespace: {{ .Values.global.istioNamespace }}
path: "/validate"
{{- end }}
rules:
- operations:
- CREATE
- UPDATE
apiGroups:
- security.istio.io
- networking.istio.io
- telemetry.istio.io
- extensions.istio.io
{{- if .Values.base.validateGateway }}
- gateway.networking.k8s.io
{{- end }}
apiVersions:
- "*"
resources:
- "*"
# Fail open until the validation webhook is ready. The webhook controller
# will update this to `Fail` and patch in the `caBundle` when the webhook
# endpoint is ready.
failurePolicy: Ignore
sideEffects: None
admissionReviewVersions: ["v1beta1", "v1"]
{{- end }}

View File

@@ -0,0 +1,23 @@
{{- if regexMatch "^([0-9]*\\.){3}[0-9]*$" .Values.global.remotePilotAddress }}
# if the remotePilotAddress is an IP addr
apiVersion: v1
kind: Endpoints
metadata:
{{- if .Values.pilot.enabled }}
name: istiod-remote
{{- else }}
name: istiod
{{- end }}
namespace: {{ .Release.Namespace }}
subsets:
- addresses:
- ip: {{ .Values.global.remotePilotAddress }}
ports:
- port: 15012
name: tcp-istiod
protocol: TCP
- port: 15017
name: tcp-webhook
protocol: TCP
---
{{- end }}

View File

@@ -0,0 +1,16 @@
# This service account aggregates reader permissions for the revisions in a given cluster
# Should be used for remote secret creation.
apiVersion: v1
kind: ServiceAccount
{{- if .Values.global.imagePullSecrets }}
imagePullSecrets:
{{- range .Values.global.imagePullSecrets }}
- name: {{ . }}
{{- end }}
{{- end }}
metadata:
name: istio-reader-service-account
namespace: {{ .Values.global.istioNamespace }}
labels:
app: istio-reader
release: {{ .Release.Name }}

View File

@@ -0,0 +1,25 @@
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# DO NOT EDIT!
# THIS IS A LEGACY CHART HERE FOR BACKCOMPAT
# UPDATED CHART AT manifests/charts/istio-control/istio-discovery
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: istiod-{{ .Values.global.istioNamespace }}
namespace: {{ .Values.global.istioNamespace }}
labels:
app: istiod
release: {{ .Release.Name }}
rules:
# permissions to verify the webhook is ready and rejecting
# invalid config. We use --server-dry-run so no config is persisted.
- apiGroups: ["networking.istio.io"]
verbs: ["create"]
resources: ["gateways"]
# For storing CA secret
- apiGroups: [""]
resources: ["secrets"]
# TODO lock this down to istio-ca-cert if not using the DNS cert mesh config
verbs: ["create", "get", "watch", "list", "update", "delete"]

View File

@@ -0,0 +1,21 @@
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# DO NOT EDIT!
# THIS IS A LEGACY CHART HERE FOR BACKCOMPAT
# UPDATED CHART AT manifests/charts/istio-control/istio-discovery
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: istiod-{{ .Values.global.istioNamespace }}
namespace: {{ .Values.global.istioNamespace }}
labels:
app: istiod
release: {{ .Release.Name }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: istiod-{{ .Values.global.istioNamespace }}
subjects:
- kind: ServiceAccount
name: istiod-service-account
namespace: {{ .Values.global.istioNamespace }}

View File

@@ -0,0 +1,19 @@
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# DO NOT EDIT!
# THIS IS A LEGACY CHART HERE FOR BACKCOMPAT
# UPDATED CHART AT manifests/charts/istio-control/istio-discovery
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
apiVersion: v1
kind: ServiceAccount
{{- if .Values.global.imagePullSecrets }}
imagePullSecrets:
{{- range .Values.global.imagePullSecrets }}
- name: {{ . }}
{{- end }}
{{- end }}
metadata:
name: istiod-service-account
namespace: {{ .Values.global.istioNamespace }}
labels:
app: istiod
release: {{ .Release.Name }}

View File

@@ -0,0 +1,28 @@
{{- if .Values.global.remotePilotAddress }}
apiVersion: v1
kind: Service
metadata:
{{- if .Values.pilot.enabled }}
# when local istiod is enabled, we can't use istiod service name to reach the remote control plane
name: istiod-remote
{{- else }}
# when local istiod isn't enabled, we can use istiod service name to reach the remote control plane
name: istiod
{{- end }}
namespace: {{ .Release.Namespace }}
spec:
ports:
- port: 15012
name: tcp-istiod
protocol: TCP
- port: 443
targetPort: 15017
name: tcp-webhook
protocol: TCP
{{- if not (regexMatch "^([0-9]*\\.){3}[0-9]*$" .Values.global.remotePilotAddress) }}
# if the remotePilotAddress is not an IP addr, we use ExternalName
type: ExternalName
externalName: {{ .Values.global.remotePilotAddress }}
{{- end }}
---
{{- end }}

View File

@@ -0,0 +1,29 @@
global:
# ImagePullSecrets for control plane ServiceAccount, list of secrets in the same namespace
# to use for pulling any images in pods that reference this ServiceAccount.
# Must be set for any cluster configured with private docker registry.
imagePullSecrets: []
# Used to locate istiod.
istioNamespace: istio-system
istiod:
enableAnalysis: false
configValidation: true
externalIstiod: false
remotePilotAddress: ""
base:
# Used for helm2 to add the CRDs to templates.
enableCRDTemplates: false
# Validation webhook configuration url
# For example: https://$remotePilotAddress:15017/validate
validationURL: ""
# For istioctl usage to disable istio config crds in base
enableIstioConfigCRDs: true
defaultRevision: "default"

View File

@@ -23,6 +23,8 @@ import (
// FS embeds the manifests
//
//go:embed profiles/*
//go:embed gatewayapi/*
//go:embed istiobase/*
var FS embed.FS
// BuiltinOrDir returns a FS for the provided directory. If no directory is passed, the compiled in

View File

@@ -1,38 +1,15 @@
# 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.
profile: kind
profile: all
global:
install: local # install mode k8s/local/docker
install: k8s # install mode k8s/local-k8s/local-docker/local
ingressClass: higress
watchNamespace:
disableAlpnH2: true
enableStatus: true
enableIstioAPI: true
enableGatewayAPI: false
namespace: higress-system
istioNamespace: istio-system
console:
port: 8080
replicas: 1
serviceType: ClusterIP
domain: console.higress.io
tlsSecretName:
webLoginPrompt:
adminPasswordValue: admin
adminPasswordLength: 8
o11yEnabled: false
pvcRwxSupported: true
gateway:
replicas: 1
@@ -44,7 +21,7 @@ controller:
replicas: 1
storage:
url: nacos://192.168.0.1:8848 # file://opt/higress/conf, buildin://127.0.0.1:8848
url: nacos://127.0.0.1:8848 # file://opt/higress/conf
ns: higress-system
username:
password:
@@ -58,8 +35,8 @@ charts:
higress:
url: https://higress.io/helm-charts
name: higress
version: 1.1.2
istio:
url: https://istio-release.storage.googleapis.com/charts
name: base
version: 1.18.2
version: latest
standalone:
url: https://higress.io/standalone/get-higress.sh
name: standalone
version: latest

View File

@@ -1,37 +1,14 @@
# 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.
profile: k8s
global:
install: k8s # install mode k8s/local-k8s/local-docker/local
ingressClass: higress
watchNamespace:
disableAlpnH2: true
enableStatus: true
enableIstioAPI: false
enableGatewayAPI: false
namespace: higress-system
istioNamespace: istio-system
console:
replicas: 1
serviceType: ClusterIP
domain: console.higress.io
tlsSecretName:
webLoginPrompt:
adminPasswordValue: admin
adminPasswordLength: 8
o11yEnabled: false
pvcRwxSupported: true
gateway:
replicas: 2
@@ -46,8 +23,8 @@ charts:
higress:
url: https://higress.io/helm-charts
name: higress
version: 1.1.2
istio:
url: https://istio-release.storage.googleapis.com/charts
name: base
version: 1.18.2
version: latest
standalone:
url: https://higress.io/standalone/get-higress.sh
name: standalone
version: latest

View File

@@ -0,0 +1,31 @@
profile: local-docker
global:
install: local-docker
console:
port: 8080
gateway:
httpPort: 80
httpsPort: 443
metricsPort: 15020
controller:
storage:
url: file://${INSTALLPACKAGEPATH}/conf
ns: higress-system
username:
password:
dataEncKey:
charts:
higress:
url: https://higress.io/helm-charts
name: higress
version: latest
standalone:
url: https://higress.io/standalone/get-higress.sh
name: standalone
version: latest

View File

@@ -1,37 +1,14 @@
# 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.
profile: local-k8s
global:
install: local-k8s # install mode k8s/local-k8s/local-docker/local
ingressClass: higress
watchNamespace:
disableAlpnH2: true
enableStatus: true
enableIstioAPI: true
enableGatewayAPI: true
namespace: higress-system
istioNamespace: istio-system
console:
replicas: 1
serviceType: ClusterIP
domain: console.higress.io
tlsSecretName:
webLoginPrompt:
adminPasswordValue: admin
adminPasswordLength: 8
o11yEnabled: true
pvcRwxSupported: true
gateway:
replicas: 1
@@ -46,8 +23,8 @@ charts:
higress:
url: https://higress.io/helm-charts
name: higress
version: 1.1.2
istio:
url: https://istio-release.storage.googleapis.com/charts
name: base
version: 1.18.2
version: latest
standalone:
url: https://higress.io/standalone/get-higress.sh
name: standalone
version: latest

View File

@@ -0,0 +1,778 @@
// 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 build
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"os/signal"
"os/user"
"strings"
"syscall"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/option"
ptypes "github.com/alibaba/higress/pkg/cmd/hgctl/plugin/types"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/utils"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
)
const (
DefaultBuilderRepository = "higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/wasm-go-builder"
DefaultBuilderGo = "1.19"
DefaultBuilderTinyGo = "0.28.1"
DefaultBuilderOras = "1.0.0"
MediaTypeSpec = "application/vnd.module.wasm.spec.v1+yaml"
MediaTypeREADME = "application/vnd.module.wasm.doc.v1+markdown"
MediaTypeREADME_ZH = "application/vnd.module.wasm.doc.v1.zh+markdown"
MediaTypeREADME_EN = "application/vnd.module.wasm.doc.v1.en+markdown"
MediaTypeIcon = "application/vnd.module.wasm.icon.v1+png"
MediaTypePlugin = "application/vnd.oci.image.layer.v1.tar+gzip"
HostTempDirPattern = "higress-wasm-go-build-*"
HostDockerEntryPattern = "higress-wasm-go-build-docker-entrypoint-*.sh"
ContainerWorkDir = "/workspace"
ContainerTempDir = "/higress_temp" // the directory to temporarily store the build products
ContainerOutDir = "/output"
ContainerDockerAuth = "/root/.docker/config.json"
ContainerEntryFile = "docker-entrypoint.sh"
ContainerEntryFilePath = "/" + ContainerEntryFile
)
type Builder struct {
OptionFile string
option.BuildOptions
Username, Password string
repository string
tempDir string
dockerEntrypoint string
uid, gid string
manualClean bool
containerID string
containerConf types.ContainerCreateConfig
dockerCli *client.Client
w io.Writer
sig chan os.Signal // watch interrupt
stop chan struct{} // stop the build process when an interruption occurs
done chan struct{} // signal that the build process is finished
utils.Debugger
*utils.YesOrNoPrinter
}
func NewBuilder(f ConfigFunc) (*Builder, error) {
b := new(Builder)
if err := b.config(f); err != nil {
return nil, err
}
return b, nil
}
func NewCommand() *cobra.Command {
var bld Builder
v := viper.New()
buildCmd := &cobra.Command{
Use: "build",
Aliases: []string{"bld", "b"},
Short: "Build Golang WASM plugin",
Example: ` # If the option.yaml file exists in the current path, do the following:
hgctl plugin build
# Using "--model(-s)" to specify the WASM plugin configuration structure name, e.g. "HelloWorldConfig"
hgctl plugin build --model HelloWorldConfig
# Using "--output-type(-t)" and "--output-dest(-d)" to push the build products as an OCI image to the specified repository
docker login
hgctl plugin build -s BasicAuthConfig -t image -d docker.io/<your_username>/<your_image>
`,
PreRun: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(bld.config(func(b *Builder) error {
return b.parseOptions(v, cmd)
}))
},
Run: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(bld.Build())
},
}
bld.bindFlags(v, buildCmd.PersistentFlags())
return buildCmd
}
func (b *Builder) bindFlags(v *viper.Viper, flags *pflag.FlagSet) {
option.AddOptionFileFlag(&b.OptionFile, flags)
flags.StringVarP(&b.Username, "username", "u", "", "Username for pushing image to the docker repository")
flags.StringVarP(&b.Password, "password", "p", "", "Password for pushing image to the docker repository")
v.BindPFlags(flags)
// this binding ensures that flags explicitly set on the command line have the
// highest priority, and if they are not set, they are read from the configuration file.
flags.StringP("builder-go", "g", DefaultBuilderGo, "Golang version in the official builder image")
v.BindPFlag("build.builder.go", flags.Lookup("builder-go"))
v.SetDefault("build.builder.go", DefaultBuilderGo)
flags.StringP("builder-tinygo", "n", DefaultBuilderTinyGo, "TinyGo version in the official builder image")
v.BindPFlag("build.builder.tinygo", flags.Lookup("builder-tinygo"))
v.SetDefault("build.builder.tinygo", DefaultBuilderTinyGo)
flags.StringP("builder-oras", "r", DefaultBuilderOras, "ORAS version in official the builder image")
v.BindPFlag("build.builder.oras", flags.Lookup("builder-oras"))
v.SetDefault("build.builder.oras", DefaultBuilderOras)
flags.StringP("input", "i", "./", "Directory of the WASM plugin project to be built")
v.BindPFlag("build.input", flags.Lookup("input"))
v.SetDefault("build.input", "./")
flags.StringP("output-type", "t", "files", "Output type of the build products. [files, image]")
v.BindPFlag("build.output.type", flags.Lookup("output-type"))
v.SetDefault("build.output.type", "files")
flags.StringP("output-dest", "d", "./out", "Output destination of the build products")
v.BindPFlag("build.output.dest", flags.Lookup("output-dest"))
v.SetDefault("build.output.dest", "./out")
flags.StringP("docker-auth", "a", "~/.docker/config.json", "Authentication configuration for pushing image to the docker repository")
v.BindPFlag("build.docker-auth", flags.Lookup("docker-auth"))
v.SetDefault("build.docker-auth", "~/.docker/config.json")
flags.StringP("model-dir", "m", "./", "Directory of the WASM plugin configuration structure")
v.BindPFlag("build.model-dir", flags.Lookup("model-dir"))
v.SetDefault("build.model-dir", "./")
flags.StringP("model", "s", "", "Structure name of the WASM plugin configuration")
v.BindPFlag("build.model", flags.Lookup("model"))
v.SetDefault("build.model", "PluginConfig")
flags.BoolP("debug", "", false, "Enable debug mode")
v.BindPFlag("build.debug", flags.Lookup("debug"))
v.SetDefault("build.debug", false)
}
func (b *Builder) Build() (err error) {
b.Debugf("build options: \n%s\n", b.String())
go func() {
err = b.doBuild()
}()
// wait for an interruption to occur or finishing the build
select {
case <-b.sig:
b.interrupt()
b.Nof("\nInterrupt ...\n")
// wait for the doBuild process to exit, otherwise there will be unexpected bugs
b.waitForFinished()
// if the build process is interrupted, then we ignore the flag `manualClean` and clean up
// TODO(WeixinX): How do we clean up uploaded image when an interruption occurs?
b.Debugln("clean up for interrupting ...")
b.CleanupForError()
os.Exit(0)
case <-b.done:
if err != nil {
if !b.manualClean {
b.Debugln("clean up for error ...")
b.CleanupForError()
}
return
}
if !b.manualClean {
b.Debugln("clean up for normal ...")
b.Cleanup()
}
}
return
}
var (
waitIcon = "[-]"
successfulIcon = "[√]"
)
func (b *Builder) doBuild() (err error) {
// finish here does not mean that the build was successful,
// but that the doBuild process is complete
defer b.finish()
if err = b.generateMetadata(); err != nil {
return errors.Wrap(err, "failed to generate wasm plugin metadata files")
}
b.Printf("%s pull the builder image ...\n", waitIcon)
ctx := context.TODO()
if err = b.imagePull(ctx); err != nil {
return errors.Wrapf(err, "failed to pull the builder image %s", b.builderImageRef())
}
b.Yesf("%s pull the builder image: %s\n", successfulIcon, b.builderImageRef())
if err = b.addContainerConfByOutType(); err != nil {
return errors.Wrapf(err, "failed to add the additional container configuration for output type %q", b.Output.Type)
}
b.Printf("%s create the builder container ...\n", waitIcon)
if err = b.containerCreate(ctx); err != nil {
return errors.Wrap(err, "failed to create the builder container")
}
b.Yesf("%s create the builder container: %s\n", successfulIcon, b.containerID)
b.Printf("%s start the builder container ...\n", waitIcon)
if err = b.containerStart(ctx); err != nil {
return errors.Wrap(err, "failed to start the builder container")
}
if b.Output.Type == "files" {
b.Yesf("%s finish building!\n", successfulIcon)
} else if b.Output.Type == "image" {
b.Yesf("%s finish building and pushing!\n", successfulIcon)
}
return nil
}
var errBuildAbort = errors.New("build aborted")
func (b *Builder) generateMetadata() error {
// spec.yaml
if b.isInterrupted() {
return errBuildAbort
}
spec, err := os.Create(b.SpecYAMLPath())
if err != nil {
return err
}
defer spec.Close()
meta, err := ptypes.ParseGoSrc(b.ModelDir, b.Model)
if err != nil {
return err
}
if err = utils.MarshalYamlWithIndentTo(spec, meta, 2); err != nil {
return err
}
// TODO(WeixinX): More languages need to be supported
// README.md is required, README_{lang}.md is optional
if b.isInterrupted() {
return errBuildAbort
}
usages, err := meta.GetUsages()
if err != nil {
return errors.Wrap(err, "failed to get wasm usage")
}
for i, u := range usages {
// since `usages` are ordered by `I18nType` and currently only `en-US` and
// `zh-CN` are available, en-US is the default README.md language when en-US is
// present (because after sorting it is in the first place)
suffix := true
if i == 0 {
suffix = false
}
if err = genMarkdownUsage(&u, b.tempDir, suffix); err != nil {
return err
}
}
return nil
}
func (b *Builder) imagePull(ctx context.Context) error {
if b.isInterrupted() {
return errBuildAbort
}
r, err := b.dockerCli.ImagePull(ctx, b.builderImageRef(), types.ImagePullOptions{})
if err != nil {
return err
}
if b.isInterrupted() {
return errBuildAbort
}
io.Copy(b.w, r)
return nil
}
func (b *Builder) addContainerConfByOutType() error {
if b.isInterrupted() {
return errBuildAbort
}
var err error
switch b.Output.Type {
case "files":
err = b.filesHandler()
case "image":
err = b.imageHandler()
default:
return errors.New("invalid output option, output type is unknown")
}
if err != nil {
return err
}
return nil
}
func (b *Builder) containerCreate(ctx context.Context) error {
if b.isInterrupted() {
return errBuildAbort
}
resp, err := b.dockerCli.ContainerCreate(ctx, b.containerConf.Config, b.containerConf.HostConfig,
b.containerConf.NetworkingConfig, b.containerConf.Platform, b.containerConf.Name)
if err != nil {
return err
}
b.containerID = resp.ID
return nil
}
func (b *Builder) containerStart(ctx context.Context) error {
if b.isInterrupted() {
return errBuildAbort
}
if err := b.dockerCli.ContainerStart(ctx, b.containerID, types.ContainerStartOptions{}); err != nil {
return err
}
if b.isInterrupted() {
return errBuildAbort
}
statusCh, errCh := b.dockerCli.ContainerWait(ctx, b.containerID, container.WaitConditionNotRunning)
select {
case err := <-errCh:
if err != nil {
return err
}
case <-statusCh:
}
if b.isInterrupted() {
return errBuildAbort
}
logs, err := b.dockerCli.ContainerLogs(ctx, b.containerID, types.ContainerLogsOptions{ShowStdout: true, ShowStderr: true})
if err != nil {
return err
}
if b.isInterrupted() {
return errBuildAbort
}
_, err = stdcopy.StdCopy(b.w, b.w, logs)
if err != nil {
return err
}
return nil
}
var errWriteDockerEntrypoint = errors.New("failed to write docker entrypoint")
func (b *Builder) filesHandler() error {
b.containerConf.HostConfig.Mounts = append(b.containerConf.HostConfig.Mounts, mount.Mount{
// output dir for the build products
Type: mount.TypeBind,
Source: b.Output.Dest,
Target: ContainerOutDir,
})
ft := &FilesTmplFields{
BuildSrcDir: ContainerWorkDir,
BuildDestDir: ContainerTempDir,
Output: ContainerOutDir,
UID: b.uid,
GID: b.uid,
Debug: b.Debug,
}
if err := genFilesDockerEntrypoint(ft, b.dockerEntrypoint); err != nil {
return errors.Wrap(err, errWriteDockerEntrypoint.Error())
}
return nil
}
var (
optionalProducts = [][2]string{
{"README_ZH.md", MediaTypeREADME_ZH},
{"README_EN.md", MediaTypeREADME_EN},
{"icon.png", MediaTypeIcon},
}
)
// TODO(WeixinX): If the image exists, no push is performed
func (b *Builder) imageHandler() error {
products := ""
for i, p := range optionalProducts {
fileName := p[0]
mediaType := p[1]
if i == 0 {
products = fmt.Sprintf("%s %s", fileName, mediaType)
} else {
products = fmt.Sprintf("%s %s %s", products, fileName, mediaType)
}
}
// spec.yaml, README.md and plugin.tar.gz are required
basicCmd := fmt.Sprintf("oras push %s -u %s -p %s ./spec.yaml:%s ./README.md:%s",
b.Output.Dest, b.Username, b.Password, MediaTypeSpec, MediaTypeREADME)
if b.Username == "" || b.Password == "" {
basicCmd = fmt.Sprintf("oras push %s ./spec.yaml:%s ./README.md:%s",
b.Output.Dest, MediaTypeSpec, MediaTypeREADME)
b.containerConf.HostConfig.Mounts = append(b.containerConf.HostConfig.Mounts, mount.Mount{
// docker auth
Type: mount.TypeBind,
Source: b.DockerAuth,
Target: ContainerDockerAuth,
})
}
it := &ImageTmplFields{
BuildSrcDir: ContainerWorkDir,
BuildDestDir: ContainerTempDir,
Output: ContainerOutDir,
Username: b.Username,
Password: b.Password,
BasicCmd: basicCmd,
Products: products,
MediaTypePlugin: MediaTypePlugin,
Debug: b.Debug,
}
if err := genImageDockerEntrypoint(it, b.dockerEntrypoint); err != nil {
return errors.Wrap(err, errWriteDockerEntrypoint.Error())
}
return nil
}
// ConfigFunc is customized to set the fields of Builder
type ConfigFunc func(b *Builder) error
func (b *Builder) config(f ConfigFunc) (err error) {
if err = f(b); err != nil {
return err
}
// builder-go
b.Builder.Go = strings.TrimSpace(b.Builder.Go)
if b.Builder.Go == "" {
b.Builder.Go = DefaultBuilderGo
}
// builder-tinygo
b.Builder.TinyGo = strings.TrimSpace(b.Builder.TinyGo)
if b.Builder.TinyGo == "" {
b.Builder.TinyGo = DefaultBuilderTinyGo
}
// builder-oras
b.Builder.Oras = strings.TrimSpace(b.Builder.Oras)
if b.Builder.Oras == "" {
b.Builder.Oras = DefaultBuilderOras
}
// input
b.Input = strings.TrimSpace(b.Input)
if b.Input == "" {
b.Input = "./"
}
inp, err := utils.GetAbsolutePath(b.Input)
if err != nil {
return errors.Wrapf(err, "failed to parse input option %q", b.Input)
}
b.Input = inp
// output-type
b.Output.Type = strings.ToLower(strings.TrimSpace(b.Output.Type))
if b.Output.Type == "" {
b.Output.Type = "files"
}
if b.Output.Type != "files" && b.Output.Type != "image" {
return errors.Errorf("invalid output type: %q, must be `files` or `image`", b.Output.Type)
}
// output-dest
b.Output.Dest = strings.TrimSpace(b.Output.Dest)
if b.Output.Dest == "" {
b.Output.Dest = "./out"
}
out := b.Output.Dest
if b.Output.Type == "files" {
out, err = utils.GetAbsolutePath(b.Output.Dest)
if err != nil {
return errors.Wrapf(err, "failed to parse output destination %q", b.Output.Dest)
}
err = os.MkdirAll(b.Output.Dest, 0755)
if err != nil && !os.IsExist(err) {
return errors.Wrapf(err, "failed to create output destination %q", b.Output.Dest)
}
}
b.Output.Dest = out
// docker-auth
b.DockerAuth = strings.TrimSpace(b.DockerAuth)
if b.DockerAuth == "" {
b.DockerAuth = "~/.docker/config.json"
}
auth, err := utils.GetAbsolutePath(b.DockerAuth)
if err != nil {
return errors.Wrapf(err, "failed to parse docker authentication %q", b.DockerAuth)
}
b.DockerAuth = auth
// model-dir
b.ModelDir = strings.TrimSpace(b.ModelDir)
if b.ModelDir == "" {
b.ModelDir = "./"
}
// option-file/username/password/model/debug: nothing to deal with
// the unexported fields that users do not need to care about are as follows:
b.repository = DefaultBuilderRepository
b.tempDir, err = os.MkdirTemp("", HostTempDirPattern)
if err != nil && !os.IsExist(err) {
return errors.Wrap(err, "failed to create the host temporary dir")
}
dockerEp, err := os.CreateTemp("", HostDockerEntryPattern)
if err != nil && !os.IsExist(err) {
return errors.Wrap(err, "failed to create the docker entrypoint file")
}
err = dockerEp.Chmod(0777)
if err != nil {
return err
}
b.dockerEntrypoint = dockerEp.Name()
dockerEp.Close()
u, err := user.Current()
if err != nil {
return errors.Wrap(err, "failed to get the current user information")
}
b.uid, b.gid = u.Uid, u.Gid
b.containerConf = types.ContainerCreateConfig{
Name: "higress-wasm-go-builder",
Config: &container.Config{
Image: b.builderImageRef(),
Env: []string{
"GO111MODULE=on",
"GOPROXY=https://goproxy.cn,direct",
},
WorkingDir: ContainerWorkDir,
Entrypoint: []string{ContainerEntryFilePath},
},
HostConfig: &container.HostConfig{
NetworkMode: "host",
Mounts: []mount.Mount{
{ // input dir that includes the wasm plugin source: main.go ...
Type: mount.TypeBind,
Source: b.Input,
Target: ContainerWorkDir,
},
{ // temp dir that includes the wasm plugin metadata: spec.yaml and README.md ...
Type: mount.TypeBind,
Source: b.tempDir,
Target: ContainerTempDir,
},
{ // entrypoint
Type: mount.TypeBind,
Source: b.dockerEntrypoint,
Target: ContainerEntryFilePath,
},
},
},
}
if b.dockerCli == nil {
b.dockerCli, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return errors.Wrap(err, "failed to initialize the docker client")
}
}
if b.w == nil {
b.w = os.Stdout
}
b.sig = make(chan os.Signal, 1)
b.stop = make(chan struct{}, 1)
b.done = make(chan struct{}, 1)
signal.Notify(b.sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM,
syscall.SIGUSR1, syscall.SIGUSR2, syscall.SIGQUIT, syscall.SIGTSTP)
if b.Debugger == nil {
b.Debugger = utils.NewDefaultDebugger(b.Debug, b.w)
}
if b.YesOrNoPrinter == nil {
b.YesOrNoPrinter = utils.NewPrinter(b.w, utils.DefaultIdent, utils.DefaultYes, utils.DefaultNo)
}
return nil
}
func (b *Builder) parseOptions(v *viper.Viper, cmd *cobra.Command) error {
allOpt, err := option.ParseOptions(b.OptionFile, v, cmd.PersistentFlags())
if err != nil {
return err
}
b.BuildOptions = allOpt.Build
b.w = cmd.OutOrStdout()
return nil
}
func (b *Builder) finish() {
select {
case <-b.done:
default:
close(b.done)
}
}
func (b *Builder) waitForFinished() {
<-b.done
}
func (b *Builder) interrupt() {
select {
case <-b.stop:
default:
close(b.stop)
}
}
func (b *Builder) isInterrupted() bool {
if b.stop == nil {
return true
}
select {
case <-b.stop:
return true
default:
return false
}
}
// WithManualClean if set this option, then the temporary files and the container
// will not be cleaned up automatically, and you need to clean up manually
func (b *Builder) WithManualClean() {
b.manualClean = true
}
func (b *Builder) WithWriter(w io.Writer) {
b.w = w
}
// CleanupForError cleans up the temporary files and the container when an error occurs
func (b *Builder) CleanupForError() {
b.Cleanup()
b.removeOutputDest()
}
// Cleanup cleans up the temporary files and the container
func (b *Builder) Cleanup() {
b.removeTempDir()
b.removeDockerEntrypoint()
b.removeBuilderContainer()
b.closeDockerCli()
}
func (b *Builder) removeOutputDest() {
if b.BuildOptions.Output.Type == "files" {
b.Debugf("remove output destination %q\n", b.BuildOptions.Output.Dest)
os.RemoveAll(b.BuildOptions.Output.Dest)
}
}
func (b *Builder) removeTempDir() {
if b.tempDir != "" {
b.Debugf("remove temporary directory %q\n", b.tempDir)
os.RemoveAll(b.tempDir)
}
}
func (b *Builder) removeDockerEntrypoint() {
if b.dockerEntrypoint != "" {
b.Debugf("delete docker entrypoint %q\n", b.dockerEntrypoint)
os.Remove(b.dockerEntrypoint)
}
}
func (b *Builder) removeBuilderContainer() {
if b.containerID != "" {
err := b.dockerCli.ContainerRemove(context.TODO(), b.containerID, types.ContainerRemoveOptions{Force: true})
if err != nil {
b.Debugf("failed to remove container (%s): %s\n", b.containerConf.Name, b.containerID)
} else {
b.Debugf("remove container (%s): %s\n", b.containerConf.Name, b.containerID)
}
}
}
func (b *Builder) closeDockerCli() {
if b.dockerCli != nil {
b.Debugln("close the docker client")
b.dockerCli.Close()
}
}
func (b *Builder) builderImageRef() string {
return fmt.Sprintf("%s:go%s-tinygo%s-oras%s", b.repository, b.Builder.Go, b.Builder.TinyGo, b.Builder.Oras)
}
func (b *Builder) SpecYAMLPath() string {
return fmt.Sprintf("%s/spec.yaml", b.tempDir)
}
func (b *Builder) TempDir() string {
return b.tempDir
}
func (b *Builder) String() string {
by, err := json.MarshalIndent(b, "", " ")
if err != nil {
return ""
}
return string(by)
}

View File

@@ -0,0 +1,194 @@
// 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 build
import (
"os"
"text/template"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/types"
)
const (
filesDockerEntrypoint = `#!/bin/bash
set -e
{{- if eq .Debug true }}
set -x
{{- end }}
go mod tidy
tinygo build -o {{ .BuildDestDir }}/plugin.wasm -scheduler=none -gc=custom -tags='custommalloc nottinygc_finalizer' -target=wasi {{ .BuildSrcDir }}
mv {{ .BuildDestDir }}/* {{ .Output }}/
chown -R {{ .UID }}:{{ .GID }} {{ .Output }}
`
imageDockerEntrypoint = `#!/bin/bash
set -e
{{- if eq .Debug true }}
set -x
{{- end }}
go mod tidy
tinygo build -o {{ .BuildDestDir }}/plugin.wasm -scheduler=none -gc=custom -tags='custommalloc nottinygc_finalizer' -target=wasi {{ .BuildSrcDir }}
cd {{ .BuildDestDir }}
tar czf plugin.tar.gz plugin.wasm
cmd="{{ .BasicCmd }}"
products=({{ .Products }})
for ((i=0; i<${#products[*]}; i=i+2)); do
f=${products[i]}
typ=${products[i+1]}
if [ -e ${f} ]; then
cmd="${cmd} ./${f}:${typ}"
fi
done
cmd="${cmd} ./plugin.tar.gz:{{ .MediaTypePlugin }}"
eval ${cmd}
`
)
type FilesTmplFields struct {
BuildSrcDir string
BuildDestDir string
Output string
UID, GID string
Debug bool
}
type ImageTmplFields struct {
BuildSrcDir string
BuildDestDir string
Output string
Username, Password string
BasicCmd string
Products string
MediaTypePlugin string
Debug bool
}
func genFilesDockerEntrypoint(ft *FilesTmplFields, target string) error {
f, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY, 0777)
if err != nil {
return err
}
defer f.Close()
if err = template.Must(template.New("FilesDockerEntrypoint").Parse(filesDockerEntrypoint)).Execute(f, ft); err != nil {
return err
}
return nil
}
func genImageDockerEntrypoint(it *ImageTmplFields, target string) error {
f, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY, 0777)
if err != nil {
return err
}
defer f.Close()
if err = template.Must(template.New("ImageDockerEntrypoint").Parse(imageDockerEntrypoint)).Execute(f, it); err != nil {
return err
}
return nil
}
const (
readme_zh_CN = `> 该插件用法文件根据源代码自动生成,请根据需求自行修改!
# 功能说明
{{ .Description }}
# 配置字段
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
| -------- | -------- | -------- | -------- | -------- |
{{- range .ConfigEntries }}
| {{ .Name }} | {{ .Type }} | {{ .Requirement }} | {{ .Default }} | {{ .Description }} |
{{- end }}
# 配置示例
` + "```yaml" + `
{{ .Example }}
` + "```" + `
`
readme_en_US = `> THIS PLUGIN USAGE FILE IS AUTOMATICALLY GENERATED BASED ON THE SOURCE CODE. MODIFY IT AS REQUIRED!
# Description
{{ .Description }}
# Configuration
| Name | Type | Requirement | Default | Description |
| -------- | -------- | -------- | -------- | -------- |
{{- range .ConfigEntries }}
| {{ .Name }} | {{ .Type }} | {{ .Requirement }} | {{ .Default }} | {{ .Description }} |
{{- end }}
# Examples
` + "```yaml" + `
{{ .Example }}
` + "```" + `
`
)
func genMarkdownUsage(u *types.WasmUsage, dir string, suffix bool) error {
md, err := os.Create(i18n2MDTitle(u.I18nType, dir, suffix))
if err != nil {
return err
}
defer md.Close()
if err = template.Must(template.New("MD_Usage").Parse(i18n2MD(u.I18nType))).Execute(md, u); err != nil {
return err
}
return nil
}
func i18n2MD(i18n types.I18nType) string {
switch i18n {
case types.I18nEN_US:
return readme_en_US
case types.I18nZH_CN:
return readme_zh_CN
default:
return readme_zh_CN
}
}
func i18n2MDTitle(i18n types.I18nType, dir string, suffix bool) string {
var file string
if !suffix {
file = "README.md"
} else {
switch i18n {
case types.I18nEN_US:
file = "README_EN.md"
case types.I18nZH_CN:
file = "README_ZH.md"
default:
file = "README_ZH.md"
}
}
return dir + "/" + file
}

View File

@@ -0,0 +1,30 @@
// 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 config
import "github.com/spf13/cobra"
func NewCommand() *cobra.Command {
configCmd := &cobra.Command{
Use: "config",
Aliases: []string{"conf", "cnf"},
Short: "Configure the WasmPlugin manifest",
}
configCmd.AddCommand(newCreateCommand())
configCmd.AddCommand(newEditCommand())
return configCmd
}

View File

@@ -0,0 +1,76 @@
// 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 config
import (
"fmt"
"io"
"os"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/utils"
"github.com/pkg/errors"
"github.com/spf13/cobra"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
)
func newCreateCommand() *cobra.Command {
var target string
createCmd := &cobra.Command{
Use: "create",
Aliases: []string{"c"},
Short: "Create the WASM plugin configuration template file",
Example: ` hgctl plugin config create`,
Run: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(create(cmd.OutOrStdout(), target))
},
}
createCmd.PersistentFlags().StringVarP(&target, "target", "t", "./", "Directory where the configuration is generated")
return createCmd
}
func create(w io.Writer, target string) error {
target, err := utils.GetAbsolutePath(target)
if err != nil {
return errors.Wrap(err, "invalid target path")
}
if err = os.MkdirAll(target, 0755); err != nil {
return err
}
if err = GenPluginConfYAML(configHelpTmpl, target); err != nil {
return errors.Wrap(err, "failed to create configuration template")
}
fmt.Fprintf(w, "Created configuration template %q\n", fmt.Sprintf("%s/%s", target, "plugin-conf.yaml"))
return nil
}
var configHelpTmpl = &PluginConf{
Name: "Plugin Name",
Namespace: "higress-system",
Title: "Display Name",
Description: "Plugin Description",
IconUrl: "Plugin Icon",
Version: "0.1.0",
Category: "auth | security | protocol | flow-control | flow-monitor | custom",
Phase: "UNSPECIFIED_PHASE | AUTHN | AUTHZ | STATS",
Priority: 0,
Config: " Plugin Configuration",
Url: "Plugin Image URL",
}

View File

@@ -0,0 +1,143 @@
// 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 config
import (
"bytes"
"context"
"fmt"
"io"
"os"
k8s "github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
"github.com/alibaba/higress/pkg/cmd/options"
"github.com/pkg/errors"
"github.com/spf13/cobra"
k8serr "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/cli-runtime/pkg/printers"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/cmd/util/editor"
)
func newEditCommand() *cobra.Command {
var name string
editCmd := &cobra.Command{
Use: "edit",
Aliases: []string{"e"},
Short: "Edit the installed WASM plugin configuration",
Example: ` # Edit the installed WASM plugin 'request-block'
hgctl plugin config edit -p request-block
`,
Run: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(edit(cmd.OutOrStdout(), name))
},
}
flags := editCmd.PersistentFlags()
options.AddKubeConfigFlags(flags)
k8s.AddHigressNamespaceFlags(flags)
flags.StringVarP(&name, "name", "p", "", "Name of the WASM plugin that needs to be edited")
return editCmd
}
func edit(w io.Writer, name string) error {
// TODO(WeixinX): Use WasmPlugin Object type instead of Unstructured
dynCli, err := k8s.NewDynamicClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
if err != nil {
return errors.Wrap(err, "failed to build kubernetes dynamic client")
}
cli := k8s.NewWasmPluginClient(dynCli)
originalObj, err := cli.Get(context.TODO(), name)
if err != nil {
if k8serr.IsNotFound(err) {
return errors.Errorf("wasm plugin %q is not found", fmt.Sprintf("%s/%s", k8s.HigressNamespace, name))
}
return errors.Wrapf(err, "failed to get wasm plugin %q", fmt.Sprintf("%s/%s", k8s.HigressNamespace, name))
}
originalObj.SetGroupVersionKind(k8s.WasmPluginGVK)
originalObj.SetManagedFields(nil) // TODO(WeixinX): Managed Fields should be written back
buf := &bytes.Buffer{}
var wObj io.Writer = buf
printer := printers.YAMLPrinter{}
if err = printer.PrintObj(originalObj.DeepCopyObject(), wObj); err != nil {
return err
}
original := buf.Bytes()
e := editor.NewDefaultEditor(editorEnvs())
edited, file, err := e.LaunchTempFile("higress-wasm-edit-", ".yaml", buf)
if err != nil {
return errors.Wrap(err, "failed to launch editor")
}
defer os.Remove(file)
if bytes.Equal(cmdutil.StripComments(original), cmdutil.StripComments(edited)) { // no change
fmt.Fprintf(w, "edit %q canceled, no change\n",
fmt.Sprintf("%s/%s", originalObj.GetNamespace(), originalObj.GetName()))
return nil
}
var editedObj unstructured.Unstructured
eBuf := bytes.NewReader(edited)
dc := yaml.NewYAMLOrJSONDecoder(eBuf, 4096)
if err = dc.Decode(&editedObj); err != nil {
return err
}
if !keepSameMeta(&editedObj, originalObj) {
fmt.Fprintln(w, "Warning: ensure that the apiVersion, kind, namespace, and name are the same as the original and are automatically corrected")
}
ret, err := cli.Update(context.TODO(), &editedObj)
if err != nil {
return errors.Wrapf(err, "failed to update wasm plugin %q",
fmt.Sprintf("%s/%s", originalObj.GetNamespace(), originalObj.GetName()))
}
fmt.Fprintf(w, "Edited wasm plugin %q\n", fmt.Sprintf("%s/%s", ret.GetNamespace(), ret.GetName()))
return nil
}
func editorEnvs() []string {
return []string{
"KUBE_EDITOR",
"EDITOR",
}
}
// to avoid changing the apiVersion, kind, namespace and name, keep them the same as the original
func keepSameMeta(edited, original *unstructured.Unstructured) bool {
same := true
if edited.GroupVersionKind().String() != original.GroupVersionKind().String() {
edited.SetGroupVersionKind(original.GroupVersionKind())
same = false
}
if edited.GetNamespace() != original.GetNamespace() {
edited.SetNamespace(original.GetNamespace())
same = false
}
if edited.GetName() != original.GetName() {
edited.SetName(original.GetName())
same = false
}
return same
}

View File

@@ -0,0 +1,160 @@
// 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 config
import (
"encoding/json"
"fmt"
"os"
"strings"
"text/template"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/types"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/utils"
"gopkg.in/yaml.v3"
)
// TODO(WeixinX): Use 'hgctl plugin push' command to fill the image url automatically
const pluginConfYAML = `# File generated by hgctl. Modify as required.
# See: https://higress.io/zh-cn/docs/plugins/intro
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
name: {{ .Name }}
namespace: {{ .Namespace }}
annotations:
higress.io/wasm-plugin-title: {{ .Title }}
higress.io/wasm-plugin-description: {{ .Description }}
higress.io/wasm-plugin-icon: {{ .IconUrl }}
labels:
higress.io/wasm-plugin-name: {{ .Name }}
higress.io/wasm-plugin-category: {{ .Category }}
higress.io/wasm-plugin-version: {{ .Version }}
higress.io/resource-definer: higress
higress.io/wasm-plugin-built-in: "false"
spec:
phase: {{ .Phase }}
priority: {{ .Priority }}
{{ .Config }}
# Please fill the image url in according to your needs
url: {{ .Url }}
`
type PluginConf struct {
Name string
Namespace string
Title string
Description string
IconUrl string
Version string
Category string
Phase string
Priority int64
Config string
Url string
}
func (pc *PluginConf) String() string {
b, err := json.MarshalIndent(pc, "", " ")
if err != nil {
return ""
}
return string(b)
}
// GenPluginConfYAML generates plugin-conf.yaml based on the template
func GenPluginConfYAML(p *PluginConf, dir string) error {
path := fmt.Sprintf("%s/plugin-conf.yaml", dir)
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
if err = template.Must(template.New("PluginConfYAML").Parse(pluginConfYAML)).Execute(f, p); err != nil {
return err
}
return nil
}
// ExtractPluginConfFrom extracts the params of plugin-conf.yaml from spec.yaml.
// input params `config`, `url` are only used to implement the command `hgctl plugin install -g <go-project>`
func ExtractPluginConfFrom(spec *types.WasmPluginMeta, config, url string) (*PluginConf, error) {
if config == "" {
// by default, Example from spec.yaml is used as the defaultConfig for the wasm plugin
var obj map[string]interface{}
example := spec.GetConfigExample()
if err := yaml.Unmarshal([]byte(example), &obj); err != nil {
return nil, err
}
conf := struct {
DefaultConfig map[string]interface{} `yaml:"defaultConfig,omitempty"`
}{DefaultConfig: obj}
b, err := utils.MarshalYamlWithIndent(conf, 2)
if err != nil {
return nil, err
}
config = string(b)
}
pc := &PluginConf{
Name: spec.Info.Name,
Namespace: "higress-system",
Title: spec.Info.Title,
Description: spec.Info.Description,
IconUrl: spec.Info.IconUrl,
Version: spec.Info.Version,
Category: string(spec.Info.Category),
Phase: string(spec.Spec.Phase),
Priority: spec.Spec.Priority,
Config: utils.AddIndent(config, strings.Repeat(" ", 2)),
Url: url,
}
pc.withDefaultValue()
return pc, nil
}
func (pc *PluginConf) withDefaultValue() {
if pc.Name == "" {
pc.Name = "Unnamed"
}
if pc.Namespace == "" {
pc.Namespace = "higress-system"
}
if pc.Title == "" {
pc.Title = "Untitled"
}
if pc.Description == "" {
pc.Description = "No description"
}
if pc.IconUrl == "" {
pc.IconUrl = types.Category2IconUrl(types.Category(pc.Category))
}
if pc.Version == "" {
pc.Version = "0.1.0"
}
if pc.Category == "" {
pc.Category = string(types.CategoryDefault)
}
if pc.Phase == "" {
pc.Phase = string(types.PhaseDefault)
}
}

View File

@@ -0,0 +1,92 @@
// 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 plugininit
import (
"fmt"
"io"
"os"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/option"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/utils"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/pkg/errors"
"github.com/spf13/cobra"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
)
func NewCommand() *cobra.Command {
var target string
initCmd := &cobra.Command{
Use: "init",
Aliases: []string{"ini", "i"},
Short: "Initialize a Golang WASM plugin project",
Example: ` hgctl plugin init`,
Run: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(runInit(cmd.OutOrStdout(), target))
},
}
initCmd.PersistentFlags().StringVarP(&target, "target", "t", "./", "Directory where the project is initialized")
return initCmd
}
func runInit(w io.Writer, target string) (err error) {
ans := answer{}
err = utils.Ask(questions, &ans)
if err != nil {
if errors.Is(err, terminal.InterruptErr) {
fmt.Fprintf(w, "Interrupted\n")
return nil
}
return errors.Wrap(err, "failed to initialize the project")
}
target, err = utils.GetAbsolutePath(target)
if err != nil {
return errors.Wrap(err, "invalid target directory")
}
dir := fmt.Sprintf("%s/%s", target, ans.Name)
err = os.MkdirAll(dir, 0755)
defer func() {
if err != nil {
os.RemoveAll(dir)
err = errors.Wrap(err, "failed to initialize the project")
}
}()
if err != nil {
return
}
if err = genGoMain(&ans, dir); err != nil {
return errors.Wrap(err, "failed to create main.go")
}
if err = genGoMod(&ans, dir); err != nil {
return errors.Wrap(err, "failed to create go.mod")
}
if err = genGitIgnore(dir); err != nil {
return errors.Wrap(err, "failed to create .gitignore")
}
if err = option.GenOptionYAML(dir); err != nil {
return errors.Wrap(err, "failed to create option.yaml")
}
fmt.Fprintf(w, "Initialized the project in %q\n", dir)
return nil
}

View File

@@ -0,0 +1,296 @@
// 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 plugininit
import (
"fmt"
"os"
"text/template"
"github.com/AlecAivazis/survey/v2"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/types"
)
const (
goMain = `// File generated by hgctl. Modify as required.
// See: https://higress.io/zh-cn/docs/user/wasm-go#2-%E7%BC%96%E5%86%99-maingo-%E6%96%87%E4%BB%B6
package main
import (
"github.com/tidwall/gjson"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
)
func main() {
wrapper.SetCtx(
"{{ .Name }}",
wrapper.ParseConfigBy(parseConfig),
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
)
}
// @Name {{ .Name }}
// @Category {{ .Category }}
// @Phase {{ .Phase }}
// @Priority {{ .Priority }}
// @Title {{ .I18nType }} {{ .Title }}
// @Description {{ .I18nType }} {{ .Description }}
// @IconUrl {{ .IconUrl }}
// @Version {{ .Version }}
//
// @Contact.name {{ .ContactName }}
// @Contact.url {{ .ContactUrl }}
// @Contact.email {{ .ContactEmail }}
//
// @Example
// firstField: hello
// secondField: world
// @End
//
type PluginConfig struct {
// @Title 第一个字段,注解格式为 @Title [语言] [标题],语言缺省值为 en-US
// @Description 字符串的前半部分,注解格式为 @Description [语言] [描述],语言缺省值为 en-US
firstField string ` + "`required:\"true\"`" + `
// @Title en-US Second Field, annotation format is @Title [language] [title], language defaults to en-US
// @Description en-US The second half of the string, annotation format is @Description [language] [description], language defaults to en-US
secondField string ` + "`required:\"true\"`" + `
}
func parseConfig(json gjson.Result, config *PluginConfig, log wrapper.Log) error {
config.firstField = json.Get("firstField").String()
config.secondField = json.Get("secondField").String()
return nil
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) types.Action {
err := proxywasm.AddHttpRequestHeader(config.firstField, config.secondField)
if err != nil {
log.Critical("failed to set request header")
}
return types.ActionContinue
}
`
goMod = `// File generated by hgctl. Modify as required.
module {{ .Name }}
go 1.19
require (
github.com/alibaba/higress/plugins/wasm-go v0.0.0-20231019123123-86b223bc75f1
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0
github.com/tidwall/gjson v1.14.3
)
`
gitIgnore = `# File generated by hgctl. Modify as required.
*
!/.gitignore
!*.go
!go.sum
!go.mod
!LICENSE
!*.md
!*.yaml
!*.yml
!*/
/out
/test
`
)
func genGoMain(ans *answer, dir string) error {
path := fmt.Sprintf("%s/main.go", dir)
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
if err = template.Must(template.New("GoMain").Parse(goMain)).Execute(f, ans); err != nil {
return err
}
return nil
}
func genGoMod(ans *answer, dir string) error {
path := fmt.Sprintf("%s/go.mod", dir)
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
if err = template.Must(template.New("GoMod").Parse(goMod)).Execute(f, ans); err != nil {
return err
}
return nil
}
func genGitIgnore(dir string) error {
path := fmt.Sprintf("%s/.gitignore", dir)
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
if _, err = f.WriteString(gitIgnore); err != nil {
return err
}
return nil
}
// obtain parameters through command line interaction
type answer struct {
Name string
Category string
Phase string
Priority int64
I18nType string
Title string
Description string
IconUrl string
Version string
ContactName string
ContactUrl string
ContactEmail string
}
var questions = []*survey.Question{
{
Name: "Name",
Prompt: &survey.Input{
Message: "Plugin name:",
Default: "hello-world",
},
Validate: survey.Required,
},
{
Name: "Category",
Prompt: &survey.Select{
Message: "Choose a plugin category:",
Options: []string{
string(types.CategoryCustom),
string(types.CategoryAuth),
string(types.CategorySecurity),
string(types.CategoryProtocol),
string(types.CategoryFlowControl),
string(types.CategoryFlowMonitor),
},
Default: string(types.CategoryCustom),
},
Validate: survey.Required,
},
{
Name: "Phase",
Prompt: &survey.Select{
Message: "Choose a execution phase:",
Options: []string{
string(types.PhaseUnspecified),
string(types.PhaseAuthn),
string(types.PhaseAuthz),
string(types.PhaseStats),
},
Default: string(types.PhaseUnspecified),
},
Validate: survey.Required,
},
{
Name: "Priority",
Prompt: &survey.Input{
Message: "Execution priority:",
Default: "0",
},
Validate: survey.Required,
},
{
Name: "I18nType",
Prompt: &survey.Select{
Message: "Choose a language:",
Options: []string{
string(types.I18nEN_US),
string(types.I18nZH_CN),
},
Default: string(types.I18nDefault),
},
Validate: survey.Required,
},
{
Name: "Title",
Prompt: &survey.Input{
Message: "Display name in the plugin market:",
Default: "Hello World",
},
Validate: survey.Required,
},
{
Name: "Description",
Prompt: &survey.Input{
Message: "Description of the plugin functionality:",
Default: "This is a demo plugin",
},
},
{
Name: "IconUrl",
Prompt: &survey.Input{
Message: "Display icon in the plugin market:",
Default: "",
},
},
{
Name: "Version",
Prompt: &survey.Input{
Message: "Plugin version:",
Default: "0.1.0",
},
Validate: survey.Required,
},
{
Name: "ContactName",
Prompt: &survey.Input{
Message: "Name of developer:",
Default: "",
},
},
{
Name: "ContactUrl",
Prompt: &survey.Input{
Message: "Homepage of developer:",
Default: "",
},
},
{
Name: "ContactEmail",
Prompt: &survey.Input{
Message: "Email of developer:",
Default: "",
},
},
}

View File

@@ -0,0 +1,744 @@
// 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 install
import (
"fmt"
"strconv"
"strings"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/types"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/utils"
"github.com/AlecAivazis/survey/v2"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"github.com/santhosh-tekuri/jsonschema/v5"
)
const (
askInterrupted = "X Interrupted."
invalidSyntax = "X Invalid syntax."
failedToValidate = "X Failed to validate: not satisfied with schema."
addConfSuccessful = "√ Successful to add configuration."
)
var iconIdent = strings.Repeat(" ", 2)
type Asker interface {
Ask() error
}
type WasmPluginSpecConfAsker struct {
resp *WasmPluginSpecConf
ingAsk *IngressAsker
domAsk *DomainAsker
glcAsk *GlobalConfAsker
printer *utils.YesOrNoPrinter
}
func NewWasmPluginSpecConfAsker(ingAsk *IngressAsker, domAsk *DomainAsker, glcAsk *GlobalConfAsker, printer *utils.YesOrNoPrinter) *WasmPluginSpecConfAsker {
return &WasmPluginSpecConfAsker{
ingAsk: ingAsk,
domAsk: domAsk,
glcAsk: glcAsk,
printer: printer,
}
}
func (p *WasmPluginSpecConfAsker) Ask() error {
var (
wpc = NewPluginSpecConf()
globalConf map[string]interface{}
ingressRule *IngressMatchRule
domainRule *DomainMatchRule
scopeA = newScopeAsker(p.printer)
rewriteA = newRewriteAsker(p.printer)
ruleA = newRuleAsker(p.printer)
complete = false
)
for {
err := scopeA.Ask()
if err != nil {
return err
}
scope := scopeA.resp
switch scope {
case types.ScopeInstance:
err = ruleA.Ask()
if err != nil {
return err
}
rule := ruleA.resp
switch rule {
case ruleIngress:
if ingressRule != nil {
p.printer.Yesf("\n%s\n", ingressRule)
err = rewriteA.Ask()
if err != nil {
return err
}
if !rewriteA.resp {
continue
}
}
p.ingAsk.scope = scope
err = p.ingAsk.Ask()
if err != nil {
return err
}
ingressRule = p.ingAsk.resp
case ruleDomain:
if domainRule != nil {
p.printer.Yesf("\n%s\n", domainRule)
err = rewriteA.Ask()
if err != nil {
return err
}
if !rewriteA.resp {
continue
}
}
p.domAsk.scope = scope
err = p.domAsk.Ask()
if err != nil {
return err
}
domainRule = p.domAsk.resp
}
case types.ScopeGlobal:
if globalConf != nil {
b, _ := utils.MarshalYamlWithIndent(globalConf, 2)
p.printer.Yesf("\n%s\n", string(b))
err = rewriteA.Ask()
if err != nil {
return err
}
if !rewriteA.resp {
continue
}
}
p.glcAsk.scope = scope
err = p.glcAsk.Ask()
if err != nil {
return err
}
globalConf = p.glcAsk.resp
case "Complete":
complete = true
break
}
if complete {
break
}
}
if globalConf != nil {
wpc.DefaultConfig = globalConf
}
if ingressRule != nil {
wpc.MatchRules = append(wpc.MatchRules, ingressRule)
}
if domainRule != nil {
wpc.MatchRules = append(wpc.MatchRules, domainRule)
}
p.printer.Yesln("The complete configuration is as follows:")
p.printer.Yesf("\n%s\n", wpc)
p.resp = wpc
return nil
}
type IngressAsker struct {
resp *IngressMatchRule
structName string
schema *types.JSONSchemaProps
scope types.Scope
vld *jsonschema.Schema // for validation
printer *utils.YesOrNoPrinter
}
func NewIngressAsker(structName string, schema *types.JSONSchemaProps, vld *jsonschema.Schema, printer *utils.YesOrNoPrinter) *IngressAsker {
return &IngressAsker{
structName: structName,
schema: schema,
vld: vld,
printer: printer,
}
}
func (i *IngressAsker) Ask() error {
continueA := newContinueAsker(i.printer)
ings := make([]string, 0)
for {
var ing string
err := utils.AskOne(&survey.Input{
Message: "Enter the matched ingress:",
Help: "Matching ingress resource object, the matching format is: namespace/ingress name",
}, &ing)
if err != nil {
return err
}
ing = strings.TrimSpace(ing)
if ing != "" {
ings = append(ings, ing)
}
err = continueA.Ask()
if err != nil {
return err
}
if !continueA.resp {
break
}
}
i.printer.Yesln(iconIdent + "Ingress:")
as, err := recursivePrompt(i.structName, i.schema, i.scope, i.printer)
if err != nil {
return err
}
if ok, ve := validate(i.vld, as); !ok {
i.printer.Noln(failedToValidate)
i.printer.Noln(ve)
return nil
}
i.resp = &IngressMatchRule{
Ingress: ings,
Config: as,
}
i.printer.Yesln(addConfSuccessful)
return nil
}
type DomainAsker struct {
resp *DomainMatchRule
structName string
schema *types.JSONSchemaProps
scope types.Scope
vld *jsonschema.Schema // for validation
printer *utils.YesOrNoPrinter
}
func NewDomainAsker(structName string, schema *types.JSONSchemaProps, vld *jsonschema.Schema, printer *utils.YesOrNoPrinter) *DomainAsker {
return &DomainAsker{
structName: structName,
schema: schema,
vld: vld,
printer: printer,
}
}
func (d *DomainAsker) Ask() error {
continueA := newContinueAsker(d.printer)
doms := make([]string, 0)
for {
var dom string
err := utils.AskOne(&survey.Input{
Message: "Enter the matched domain:",
Help: "match domain name, support generic domain name",
}, &dom)
if err != nil {
return err
}
dom = strings.TrimSpace(dom)
if dom != "" {
doms = append(doms, dom)
}
err = continueA.Ask()
if err != nil {
return err
}
if !continueA.resp {
break
}
}
d.printer.Yesln(iconIdent + "Domain:")
as, err := recursivePrompt(d.structName, d.schema, d.scope, d.printer)
if err != nil {
return err
}
if ok, ve := validate(d.vld, as); !ok {
d.printer.Noln(failedToValidate)
d.printer.Noln(ve)
return nil
}
d.resp = &DomainMatchRule{
Domain: doms,
Config: as,
}
d.printer.Yesln(addConfSuccessful)
return nil
}
type GlobalConfAsker struct {
resp map[string]interface{}
structName string
schema *types.JSONSchemaProps
scope types.Scope
vld *jsonschema.Schema // for validation
printer *utils.YesOrNoPrinter
}
func NewGlobalConfAsker(structName string, schema *types.JSONSchemaProps, vld *jsonschema.Schema, printer *utils.YesOrNoPrinter) *GlobalConfAsker {
return &GlobalConfAsker{
structName: structName,
schema: schema,
vld: vld,
printer: printer,
}
}
func (g *GlobalConfAsker) Ask() error {
g.printer.Yesln(iconIdent + "Global:")
as, err := recursivePrompt(g.structName, g.schema, g.scope, g.printer)
if err != nil {
return err
}
if ok, ve := validate(g.vld, as); !ok {
g.printer.Noln(failedToValidate)
g.printer.Noln(ve)
return nil
}
g.resp = as.(map[string]interface{})
g.printer.Yesln(addConfSuccessful)
return nil
}
type continueAsker struct {
resp bool
printer *utils.YesOrNoPrinter
}
func newContinueAsker(printer *utils.YesOrNoPrinter) *continueAsker {
return &continueAsker{printer: printer}
}
func (c *continueAsker) Ask() error {
resp := true
err := utils.AskOne(&survey.Confirm{
Message: fmt.Sprintf("%scontinue?", c.printer.Ident()),
Default: true,
}, &resp)
if err != nil {
return err
}
c.resp = resp
return nil
}
type rewriteAsker struct {
resp bool
printer *utils.YesOrNoPrinter
}
func newRewriteAsker(printer *utils.YesOrNoPrinter) *rewriteAsker {
return &rewriteAsker{printer: printer}
}
func (r *rewriteAsker) Ask() error {
resp := false
err := utils.AskOne(&survey.Confirm{
Message: fmt.Sprintf("%sThe configuration already exists as shown above. Do you want to rewrite it?", r.printer.Ident()),
Default: false,
}, &resp)
if err != nil {
return err
}
r.resp = resp
return nil
}
type scopeAsker struct {
resp types.Scope
printer *utils.YesOrNoPrinter
}
func newScopeAsker(printer *utils.YesOrNoPrinter) *scopeAsker {
return &scopeAsker{printer: printer}
}
func (s *scopeAsker) Ask() error {
var resp string
err := utils.AskOne(&survey.Select{
Message: fmt.Sprintf("%sChoose a configuration effective scope or complete:", s.printer.Ident()),
Options: []string{
// TODO(WeixinX): Not visible to the user, instead Global, Ingress, and Domain are asked in ruleAsker
string(types.ScopeInstance),
string(types.ScopeGlobal),
"Complete",
},
Default: string(types.ScopeInstance),
}, &resp)
if err != nil {
return err
}
s.resp = types.Scope(resp)
return nil
}
type ruleAsker struct {
resp Rule
printer *utils.YesOrNoPrinter
}
func newRuleAsker(printer *utils.YesOrNoPrinter) *ruleAsker {
return &ruleAsker{printer: printer}
}
func (r *ruleAsker) Ask() error {
var resp string
err := utils.AskOne(&survey.Select{
Message: fmt.Sprintf("%sChoose Ingress or Domain:", r.printer.Ident()),
Options: []string{
string(ruleIngress),
string(ruleDomain),
},
Default: string(ruleIngress),
}, &resp)
if err != nil {
return err
}
r.resp = Rule(resp)
return nil
}
type WasmPluginSpecConf struct {
DefaultConfig map[string]interface{} `yaml:"defaultConfig,omitempty"`
MatchRules []MatchRule `yaml:"matchRules,omitempty"`
}
func NewPluginSpecConf() *WasmPluginSpecConf {
return &WasmPluginSpecConf{
MatchRules: make([]MatchRule, 0),
}
}
func (p *WasmPluginSpecConf) String() string {
if len(p.DefaultConfig) == 0 && len(p.MatchRules) == 0 {
return " "
}
b, _ := utils.MarshalYamlWithIndent(p, 2)
return string(b)
}
type MatchRule interface {
String() string
}
type IngressMatchRule struct {
Ingress []string `json:"ingress" yaml:"ingress" mapstructure:"ingress"`
Config interface{} `json:"config" yaml:"config" mapstructure:"config"`
}
func (i IngressMatchRule) String() string {
b, _ := utils.MarshalYamlWithIndent(i, 2)
return string(b)
}
func decodeIngressMatchRule(obj map[string]interface{}) (*IngressMatchRule, error) {
var ing IngressMatchRule
if err := mapstructure.Decode(obj, &ing); err != nil {
return nil, err
}
return &ing, nil
}
type DomainMatchRule struct {
Domain []string `json:"domain" yaml:"domain" mapstructure:"domain"`
Config interface{} `json:"config" yaml:"config" mapstructure:"config"`
}
func (d DomainMatchRule) String() string {
b, _ := utils.MarshalYamlWithIndent(d, 2)
return string(b)
}
func decodeDomainMatchRule(obj map[string]interface{}) (*DomainMatchRule, error) {
var dom DomainMatchRule
if err := mapstructure.Decode(obj, &dom); err != nil {
return nil, err
}
return &dom, nil
}
type Rule string
const (
ruleIngress Rule = "Ingress"
ruleDomain Rule = "Domain"
)
func recursivePrompt(structName string, schema *types.JSONSchemaProps, selScope types.Scope, printer *utils.YesOrNoPrinter) (interface{}, error) {
printer.IncIdentRepeat()
defer printer.DecIndentRepeat()
return doPrompt(structName, nil, schema, types.ScopeAll, selScope, printer)
}
func doPrompt(fieldName string, parent, schema *types.JSONSchemaProps, oriScope, selScope types.Scope, printer *utils.YesOrNoPrinter) (interface{}, error) {
if schema.Title == "" {
schema.Title = fieldName
}
if schema.Description == "" {
schema.Description = fieldName
}
required := true
if parent != nil {
required = isRequired(fieldName, parent.Required)
}
msg, help := fieldTips(fieldName, parent, schema, required, printer)
switch types.JsonType(schema.Type) {
case types.JsonTypeObject:
printer.Println(iconIdent + msg)
obj := make(map[string]interface{})
m := schema.GetPropertiesOrderMap()
for _, name := range m.Keys() {
propI, _ := m.Get(name)
prop := propI.(types.JSONSchemaProps)
if parent == nil { // keep topmost scope
if prop.Scope == types.ScopeGlobal {
oriScope = types.ScopeGlobal
} else if prop.Scope == types.ScopeInstance || prop.Scope == "" {
oriScope = types.ScopeInstance
}
}
if !matchesScope(oriScope, selScope, prop.Scope) {
continue
}
printer.IncIdentRepeat()
v, err := doPrompt(name, schema, &prop, oriScope, selScope, printer)
printer.DecIndentRepeat()
if err != nil {
return nil, err
}
if v != nil {
obj[name] = v
}
}
if len(obj) == 0 {
return nil, nil
}
return obj, nil
case types.JsonTypeArray:
printer.Println(iconIdent + msg)
continueA := newContinueAsker(printer)
arr := make([]interface{}, 0)
for {
printer.IncIdentRepeat()
v, err := doPrompt("item", schema, schema.Items.Schema, oriScope, selScope, printer)
if err != nil {
printer.DecIndentRepeat()
return nil, err
}
if v != nil {
arr = append(arr, v)
}
err = continueA.Ask()
printer.DecIndentRepeat()
if err != nil {
return nil, err
}
if !continueA.resp {
break
}
}
if len(arr) == 0 {
return nil, nil
}
return arr, nil
case types.JsonTypeInteger, types.JsonTypeNumber, types.JsonTypeBoolean, types.JsonTypeString:
for {
var inp string
if err := utils.AskOne(&survey.Input{
Message: msg,
Help: help,
}, &inp); err != nil {
return nil, err
}
if inp == "" && !required {
return nil, nil
}
switch types.JsonType(schema.Type) {
case types.JsonTypeInteger:
v, err := strconv.ParseInt(inp, 10, 64)
if err != nil {
if errors.Is(err, strconv.ErrSyntax) {
printer.Nof("%s %q type is invalid.\n", invalidSyntax, inp)
continue
}
return nil, err
}
return v, nil
case types.JsonTypeNumber:
v, err := strconv.ParseFloat(inp, 64)
if err != nil {
if errors.Is(err, strconv.ErrSyntax) {
printer.Nof("%s %q type is invalid.\n", invalidSyntax, inp)
continue
}
return nil, err
}
return v, nil
case types.JsonTypeBoolean:
v, err := strconv.ParseBool(inp)
if err != nil {
if errors.Is(err, strconv.ErrSyntax) {
printer.Nof("%s %q type is invalid.\n", invalidSyntax, inp)
continue
}
return nil, err
}
return v, nil
case types.JsonTypeString:
return inp, nil
default:
return inp, nil
}
}
default:
return nil, fmt.Errorf("unsupported type: %s", schema.Type)
}
}
func matchesScope(oriScope, selScope, scope types.Scope) bool {
return (oriScope == selScope) ||
(selScope == types.ScopeInstance && (scope == selScope || scope == "" || scope == types.ScopeAll)) ||
(selScope == types.ScopeGlobal && (scope == selScope || scope == types.ScopeAll))
}
func fieldTips(fieldName string, parent, schema *types.JSONSchemaProps, required bool, printer *utils.YesOrNoPrinter) (string, string) {
var msg, help string
if fieldName == "item" {
msg = fmt.Sprintf("%s%s(%s)", printer.Ident(), fieldName, schema.Type)
help = fmt.Sprintf("%s%s: %s", printer.Ident(), parent.Title, parent.Description)
} else {
req := schema.JoinRequirementsBy(types.I18nEN_US, required)
msg = fmt.Sprintf("%s%s(%s, %s)", printer.Ident(), fieldName, schema.Type, req)
help = fmt.Sprintf("%s%s: %s", printer.Ident(), schema.Title, schema.Description)
}
return msg, help
}
func isRequired(name string, required []string) bool {
req := false
for _, n := range required {
if name == n {
req = true
break
}
}
return req
}
func validate(schema *jsonschema.Schema, v interface{}) (bool, error) {
if err := schema.Validate(v); err != nil {
err = convertValidationError(err.(*jsonschema.ValidationError))
return false, err
}
return true, nil
}
func convertValidationError(ve *jsonschema.ValidationError) error {
de := ve.DetailedOutput()
if de.Valid {
return nil
}
errs := make([]error, 0)
if de.Error != "" {
errs = append(errs, errors.New(de.Error))
}
errs = append(errs, doConvertValidationError(de.Errors, errs)...)
if len(errs) == 0 {
return nil
}
var ret error
for i, err := range errs {
if i == 0 {
ret = fmt.Errorf("%w", err)
} else {
ret = fmt.Errorf("%s\n%w", ret.Error(), err)
}
}
return ret
}
func doConvertValidationError(de []jsonschema.Detailed, errs []error) []error {
for _, e := range de {
if e.Error != "" {
errs = append(errs, errors.New(e.Error))
}
if len(e.Errors) > 0 {
errs = append(errs, doConvertValidationError(e.Errors, errs)...)
}
}
return errs
}

View File

@@ -0,0 +1,383 @@
// 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 install
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"strings"
k8s "github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/build"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/config"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/option"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/types"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/utils"
"github.com/alibaba/higress/pkg/cmd/options"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/pkg/errors"
"github.com/santhosh-tekuri/jsonschema/v5"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
k8serr "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
)
type installer struct {
optionFile string
bldOpts option.BuildOptions
insOpts option.InstallOptions
cli *k8s.WasmPluginClient
w io.Writer
utils.Debugger
}
func NewCommand() *cobra.Command {
var ins installer
v := viper.New()
installCmd := &cobra.Command{
Use: "install",
Aliases: []string{"ins", "i"},
Short: "Install WASM plugin",
Example: ` # Install WASM plugin using a WasmPlugin manifest
hgctl plugin install -y plugin-conf.yaml
# Install WASM plugin through the Golang WASM plugin project (do it by relying on option.yaml now)
docker login
hgctl plugin install -g ./
`,
PreRun: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(ins.config(v, cmd))
},
Run: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(ins.install(cmd.PersistentFlags()))
},
}
flags := installCmd.PersistentFlags()
options.AddKubeConfigFlags(flags)
option.AddOptionFileFlag(&ins.optionFile, flags)
v.BindPFlags(flags)
flags.StringP("namespace", "n", k8s.HigressNamespace, "Namespace where Higress was installed")
v.BindPFlag("install.namespace", flags.Lookup("namespace"))
v.SetDefault("install.namespace", k8s.DefaultHigressNamespace)
flags.StringP("spec-yaml", "s", "./out/spec.yaml", "Use to validate WASM plugin configuration")
v.BindPFlag("install.spec-yaml", flags.Lookup("spec-yaml"))
v.SetDefault("install.spec-yaml", "./test/plugin-spec-yaml")
// TODO(WeixinX):
// - Change "--from-yaml (-y)" to "--from-oci (-o)" and implement command line interaction like "--from-go-src"
// - Add "--from-jar (-j)"
flags.StringP("from-yaml", "y", "./test/plugin-conf.yaml", "Install WASM plugin using a WasmPlugin manifest")
v.BindPFlag("install.from-yaml", flags.Lookup("from-yaml"))
v.SetDefault("install.from-yaml", "./test/plugin-conf.yaml")
flags.StringP("from-go-src", "g", "", "Install WASM plugin through the Golang WASM plugin project")
v.BindPFlag("install.from-go-src", flags.Lookup("from-go-src"))
v.SetDefault("install.from-go-src", "")
flags.BoolP("debug", "", false, "Enable debug mode")
v.BindPFlag("install.debug", flags.Lookup("debug"))
v.SetDefault("install.debug", false)
return installCmd
}
func (ins *installer) config(v *viper.Viper, cmd *cobra.Command) error {
allOpt, err := option.ParseOptions(ins.optionFile, v, cmd.PersistentFlags())
if err != nil {
return err
}
// TODO(WeixinX): Avoid relying on build options, add a new option "--push/--image" for installing from go src
ins.bldOpts = allOpt.Build
ins.insOpts = allOpt.Install
dynCli, err := k8s.NewDynamicClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
if err != nil {
return errors.Wrap(err, "failed to build kubernetes dynamic client")
}
ins.cli = k8s.NewWasmPluginClient(dynCli)
ins.w = cmd.OutOrStdout()
ins.Debugger = utils.NewDefaultDebugger(ins.insOpts.Debug, ins.w)
return nil
}
func (ins *installer) install(flags *pflag.FlagSet) (err error) {
ins.Debugf("install option:\n%s\n", ins.String())
if ins.insOpts.FromGoSrc == "" || flags.Changed("from-yaml") {
err = ins.yamlHandler()
} else {
err = ins.goHandler()
}
return
}
func (ins *installer) yamlHandler() error {
return ins.doInstall(true)
}
func (ins *installer) goHandler() error {
// 0. ensure output.type == image
if ins.bldOpts.Output.Type != "image" {
return errors.New("output type must be image")
}
// 1. build the WASM plugin project and push the image to the registry
bld, err := build.NewBuilder(func(b *build.Builder) error {
b.BuildOptions = ins.bldOpts
b.Debug = ins.insOpts.Debug
b.WithManualClean() // keep spec.yaml
b.WithWriter(ins.w)
return nil
})
if err != nil {
return errors.Wrap(err, "failed to initialize builder")
}
err = bld.Build()
if err != nil {
bld.Debugln("clean up for error ...")
bld.CleanupForError()
return errors.Wrap(err, "failed to build and push wasm plugin")
}
defer bld.Cleanup()
// 2. command-line interaction lets the user enter the wasm plugin configuration
specPath := bld.SpecYAMLPath()
spec, err := types.ParseSpecYAML(specPath)
if err != nil {
return errors.Wrapf(err, "failed to parse spec.yaml: %s", specPath)
}
vld, err := buildSchemaValidator(spec)
if err != nil {
return err
}
example := spec.GetConfigExample()
schema := spec.Spec.ConfigSchema.OpenAPIV3Schema
printer := utils.DefaultPrinter()
asker := NewWasmPluginSpecConfAsker(
NewIngressAsker(bld.Model, schema, vld, printer),
NewDomainAsker(bld.Model, schema, vld, printer),
NewGlobalConfAsker(bld.Model, schema, vld, printer),
printer,
)
printer.Yesln("Please enter the configurations for the WASM plugin you want to install:")
printer.Yesln("Configuration example:")
printer.Yesf("\n%s\n", example)
err = asker.Ask()
if err != nil {
if errors.Is(err, terminal.InterruptErr) {
printer.Noln(askInterrupted)
return nil
}
panic(err)
}
// 3. generate the WasmPlugin manifest
wpc := asker.resp
if err != nil {
return errors.Wrap(err, "failed to marshal wasm plugin config")
}
// get the parameters of plugin-conf.yaml from spec.yaml
pc, err := config.ExtractPluginConfFrom(spec, wpc.String(), bld.Output.Dest)
if err != nil {
return errors.Wrapf(err, "failed to get the parameters of plugin-conf.yaml from %s", specPath)
}
ins.Debugf("plugin-conf.yaml params:\n%s\n", pc.String())
if err = config.GenPluginConfYAML(pc, bld.TempDir()); err != nil {
return errors.Wrap(err, "failed to generate plugin-conf.yaml")
}
// 4. install by the manifest
ins.insOpts.FromYaml = bld.TempDir() + "/plugin-conf.yaml"
if err = ins.doInstall(false); err != nil {
return err
}
return nil
}
func (ins *installer) doInstall(validate bool) error {
f, err := os.Open(ins.insOpts.FromYaml)
if err != nil {
return err
}
defer f.Close()
// multiple WASM plugins are separated by '---' in yaml, but we only handle first one
// TODO(WeixinX): Use WasmPlugin Object type instead of Unstructured
obj := &unstructured.Unstructured{}
dc := k8syaml.NewYAMLOrJSONDecoder(f, 4096)
if err = dc.Decode(obj); err != nil {
return errors.Wrapf(err, "failed to parse wasm plugin from manifest %q", ins.insOpts.FromYaml)
}
if !isValidAPIVersion(obj) {
fmt.Fprintf(ins.w, "Warning: wasm plugin %q has invalid apiVersion, automatically modified: %q -> %q\n",
obj.GetName(), obj.GetAPIVersion(), k8s.HigressExtAPIVersion)
obj.SetAPIVersion(k8s.HigressExtAPIVersion)
}
if !isValidKind(obj) {
fmt.Fprintf(ins.w, "Warning: wasm plugin %q has invalid kind, automatically modified: %q -> %q\n",
obj.GetName(), obj.GetKind(), k8s.WasmPluginKind)
obj.SetKind(k8s.WasmPluginKind)
}
if !isValidNamespace(obj) {
fmt.Fprintf(ins.w, "Warning: wasm plugin %q has invalid namespace, automatically modified: %q -> %q\n",
obj.GetName(), obj.GetNamespace(), k8s.HigressNamespace)
obj.SetNamespace(k8s.HigressNamespace)
}
// validate wasm plugin config
if validate {
if wps, ok := obj.Object["spec"].(map[string]interface{}); ok {
if err = ins.validateWasmPluginConfig(wps); err != nil {
return err
}
} else {
return errors.New("failed to get the spec filed of wasm plugin")
}
ins.Debugln("successfully validated wasm plugin config")
}
result, err := ins.cli.Create(context.TODO(), obj)
if err != nil {
if k8serr.IsAlreadyExists(err) {
fmt.Fprintf(ins.w, "wasm plugin %q already exists\n",
fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName()))
return nil
}
return errors.Wrapf(err, "failed to install wasm plugin %q",
fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName()))
}
fmt.Fprintf(ins.w, "Installed wasm plugin %q\n", fmt.Sprintf("%s/%s", result.GetNamespace(), result.GetName()))
return nil
}
func isValidAPIVersion(obj *unstructured.Unstructured) bool {
return obj.GetAPIVersion() == k8s.HigressExtAPIVersion
}
func isValidKind(obj *unstructured.Unstructured) bool {
return obj.GetKind() == k8s.WasmPluginKind
}
func isValidNamespace(obj *unstructured.Unstructured) bool {
return obj.GetNamespace() == k8s.HigressNamespace
}
func (ins *installer) validateWasmPluginConfig(wps map[string]interface{}) error {
spec, err := types.ParseSpecYAML(ins.insOpts.SpecYaml)
if err != nil {
return errors.Wrapf(err, "failed to parse %s", ins.insOpts.SpecYaml)
}
vld, err := buildSchemaValidator(spec)
if err != nil {
return errors.Wrapf(err, "failed to build schema validator")
}
if dc, ok := wps["defaultConfig"].(map[string]interface{}); ok {
if ok, err = validate(vld, dc); !ok {
return errors.Wrap(err, "failed to validate default config")
}
// debug
b, _ := utils.MarshalYamlWithIndent(dc, 2)
ins.Debugf("default config:\n%s\n", string(b))
}
if mrs, ok := wps["matchRules"].([]interface{}); ok {
for _, mr := range mrs {
if r, ok := mr.(map[string]interface{}); ok {
if _, ok = r["ingress"]; ok {
ing, err := decodeIngressMatchRule(r)
if err != nil {
return errors.Wrap(err, "failed to parse ingress match rule")
}
if ok, err = validate(vld, ing.Config); !ok {
return errors.Wrap(err, "failed to validate ingress match rule")
}
ins.Debugf("ingress match rule:\n%s\n", ing.String())
} else if _, ok = r["domain"]; ok {
dom, err := decodeDomainMatchRule(r)
if err != nil {
return errors.Wrap(err, "failed to parse domain match rule")
}
if ok, err = validate(vld, dom.Config); !ok {
return errors.Wrap(err, "failed to validate ingress match rule")
}
ins.Debugf("domain match rule:\n%s\n", dom.String())
}
}
}
}
return nil
}
func buildSchemaValidator(spec *types.WasmPluginMeta) (*jsonschema.Schema, error) {
if spec == nil {
return nil, errors.New("spec is nil")
}
schema := spec.Spec.ConfigSchema.OpenAPIV3Schema
if schema == nil {
return nil, errors.New("spec has no config schema")
}
b, err := json.Marshal(schema)
if err != nil {
return nil, err
}
c := jsonschema.NewCompiler()
c.Draft = jsonschema.Draft4
err = c.AddResource("schema.json", strings.NewReader(string(b)))
vld, err := c.Compile("schema.json")
if err != nil {
errors.Wrap(err, "failed to compile schema")
}
return vld, nil
}
func (ins *installer) String() string {
b, err := json.MarshalIndent(ins.insOpts, "", " ")
if err != nil {
return ""
}
return fmt.Sprintf("OptionFile: %s\n%s", ins.optionFile, string(b))
}

View File

@@ -0,0 +1,78 @@
// 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 ls
import (
"context"
"fmt"
"io"
"time"
k8s "github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
"github.com/alibaba/higress/pkg/cmd/options"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/util/duration"
"k8s.io/cli-runtime/pkg/printers"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
)
func NewCommand() *cobra.Command {
lsCmd := &cobra.Command{
Use: "ls",
Aliases: []string{"l"},
Short: "List all installed WASM plugins",
Example: ` hgctl plugin ls`,
Run: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(runLs(cmd.OutOrStdout()))
},
}
flags := lsCmd.PersistentFlags()
options.AddKubeConfigFlags(flags)
k8s.AddHigressNamespaceFlags(flags)
return lsCmd
}
func runLs(w io.Writer) error {
dynCli, err := k8s.NewDynamicClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
if err != nil {
return errors.Wrap(err, "failed to build kubernetes client")
}
cli := k8s.NewWasmPluginClient(dynCli)
list, err := cli.List(context.TODO())
if err != nil {
return errors.Wrap(err, "failed to list all wasm plugins")
}
printer := printers.GetNewTabWriter(w)
now := time.Now()
fmt.Fprintf(printer, "NAME\tAGE\n")
for _, item := range list.Items {
fmt.Fprintf(printer, "%s\t%s\n", item.GetName(), getAge(now, item.GetCreationTimestamp().Time))
}
if err = printer.Flush(); err != nil {
return errors.Wrap(err, "failed to flush output")
}
return nil
}
func getAge(now time.Time, create time.Time) string {
return duration.ShortHumanDuration(now.Sub(create))
}

View File

@@ -0,0 +1,96 @@
// 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 option
import (
"os"
"github.com/pkg/errors"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
type Option struct {
Version string `json:"version" yaml:"version" mapstructure:"version"`
Build BuildOptions `json:"build" yaml:"build" mapstructure:"build"`
Test TestOptions `json:"test" yaml:"test" mapstructure:"test"`
Install InstallOptions `json:"install" yaml:"install" mapstructure:"install"`
}
type BuildOptions struct {
Builder BuilderVersion `json:"builder" yaml:"builder" mapstructure:"builder"`
Input string `json:"input" yaml:"input" mapstructure:"input"`
Output Output `json:"output" yaml:"output" mapstructure:"output"`
DockerAuth string `json:"docker-auth" yaml:"docker-auth" mapstructure:"docker-auth"`
ModelDir string `json:"model-dir" yaml:"model-dir" mapstructure:"model-dir"`
Model string `json:"model" yaml:"model" mapstructure:"model"`
Debug bool `json:"debug" yaml:"debug" mapstructure:"debug"`
}
type TestOptions struct {
Name string `json:"name" yaml:"name" mapstructure:"name"`
FromPath string `json:"from-path" yaml:"from-path" mapstructure:"from-path"`
TestPath string `json:"test-path" yaml:"test-path" mapstructure:"test-path"`
ComposeFile string `json:"compose-file" yaml:"compose-file" mapstructure:"compose-file"`
Detach bool `json:"detach" yaml:"detach" mapstructure:"detach"`
}
type InstallOptions struct {
Namespace string `json:"namespace" yaml:"namespace" mapstructure:"namespace"`
SpecYaml string `json:"spec-yaml" yaml:"spec-yaml" mapstructure:"spec-yaml"`
FromYaml string `json:"from-yaml" yaml:"from-yaml" mapstructure:"from-yaml"`
FromGoSrc string `json:"from-go-src" yaml:"from-go-src" mapstructure:"from-go-src"`
Debug bool `json:"debug" yaml:"debug" mapstructure:"debug"`
}
type BuilderVersion struct {
Go string `json:"go" yaml:"go" mpastructure:"go"`
TinyGo string `json:"tinygo" yaml:"tinygo" mapstructure:"tinygo"`
Oras string `json:"oras" yaml:"oras" mapstructure:"oras"`
}
type Output struct {
Type string `json:"type" yaml:"type" mapstructure:"type"`
Dest string `json:"dest" yaml:"dest" mapstructure:"dest"`
}
// ParseOptions reads `option.yaml` and parses it into Option struct
func ParseOptions(optionFile string, v *viper.Viper, flags *pflag.FlagSet) (*Option, error) {
_, err := os.Stat(optionFile)
if err != nil {
// `option-file` is explicitly specified, but the given file does not exist
if errors.Is(err, os.ErrNotExist) && flags.Changed("option-file") {
return nil, errors.Errorf("option file does not exist: %q", optionFile)
}
} else {
v.SetConfigFile(optionFile)
if err = v.ReadInConfig(); err != nil {
return nil, errors.Wrapf(err, "failed to read option file %q", optionFile)
}
}
var opt Option
if err = v.Unmarshal(&opt); err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal option file %q", optionFile)
}
return &opt, nil
}
// AddOptionFileFlag adds `option-file` flag
func AddOptionFileFlag(optionFile *string, flags *pflag.FlagSet) {
flags.StringVarP(optionFile, "option-file", "f", "./option.yaml",
"Option file for build, test and install")
}

View File

@@ -0,0 +1,89 @@
// 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 option
import (
"fmt"
"os"
)
const optionYAML = `# File generated by hgctl. Modify as required.
version: 1.0.0
build:
# The official builder image version
builder:
go: 1.19
tinygo: 0.28.1
oras: 1.0.0
# The WASM plugin project directory
input: ./
# The output of the build products
output:
# Choose between 'files' and 'image'
type: files
# Destination address: when type=files, specify the local directory path, e.g., './out' or
# type=image, specify the remote docker repository, e.g., 'docker.io/<your_username>/<your_image>'
dest: ./out
# The authentication configuration for pushing image to the docker repository
docker-auth: ~/.docker/config.json
# The directory for the WASM plugin configuration structure
model-dir: ./
# The WASM plugin configuration structure name
model: PluginConfig
# Enable debug mode
debug: false
test:
# Test environment name, that is a docker compose project name
name: wasm-test
# The output path to build products, that is the source of test configuration parameters
from-path: ./out
# The test configuration source
test-path: ./test
# Docker compose configuration, which is empty, looks for the following files from 'test-path':
# compose.yaml, compose.yml, docker-compose.yml, docker-compose.yaml
compose-file:
# Detached mode: Run containers in the background
detach: false
install:
# The namespace of the installation
namespace: higress-system
# Use to validate WASM plugin configuration when install by yaml
spec-yaml: ./out/spec.yaml
# Installation source. Choose between 'from-yaml' and 'from-go-project'
from-yaml: ./test/plugin-conf.yaml
# If 'from-go-src' is non-empty, the output type of the build option must be 'image'
from-go-src:
# Enable debug mode
debug: false
`
func GenOptionYAML(dir string) error {
path := fmt.Sprintf("%s/option.yaml", dir)
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
if _, err = f.WriteString(optionYAML); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,45 @@
// 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 plugin
import (
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/build"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/config"
plugininit "github.com/alibaba/higress/pkg/cmd/hgctl/plugin/init"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/install"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/ls"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/test"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/uninstall"
"github.com/spf13/cobra"
)
func NewCommand() *cobra.Command {
pluginCommand := &cobra.Command{
Use: "plugin",
Aliases: []string{"plg", "p"},
Short: "For the Golang WASM plugin",
}
pluginCommand.AddCommand(build.NewCommand())
pluginCommand.AddCommand(install.NewCommand())
pluginCommand.AddCommand(uninstall.NewCommand())
pluginCommand.AddCommand(ls.NewCommand())
pluginCommand.AddCommand(test.NewCommand())
pluginCommand.AddCommand(config.NewCommand())
pluginCommand.AddCommand(plugininit.NewCommand())
return pluginCommand
}

View File

@@ -0,0 +1,108 @@
// 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 test
import (
"context"
"fmt"
"io"
"os"
"github.com/alibaba/higress/pkg/cmd/hgctl/docker"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/option"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/utils"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
)
type cleaner struct {
optionFile string
option.TestOptions
w io.Writer
}
func newCleanCommand() *cobra.Command {
var c cleaner
v := viper.New()
cleanCmd := &cobra.Command{
Use: "clean",
Aliases: []string{"cl"},
Short: "Clean the test environment, that is remove the source of test configuration",
Example: ` hgctl plugin test clean`,
PreRun: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(c.config(v, cmd))
},
Run: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(c.clean())
},
}
flags := cleanCmd.PersistentFlags()
option.AddOptionFileFlag(&c.optionFile, flags)
v.BindPFlags(flags)
flags.StringP("name", "p", "wasm-test", "Test environment name")
v.BindPFlag("test.name", flags.Lookup("name"))
v.SetDefault("test.name", "wasm-test")
// TODO(WeixinX): Obtain the test configuration source directory based on the test environment name (hgctl plugin test ls)
flags.StringP("test-path", "t", "./test", "Test configuration source")
v.BindPFlag("test.test-path", flags.Lookup("test-path"))
v.SetDefault("test.test-path", "./test")
return cleanCmd
}
func (c *cleaner) config(v *viper.Viper, cmd *cobra.Command) error {
allOpt, err := option.ParseOptions(c.optionFile, v, cmd.PersistentFlags())
if err != nil {
return err
}
c.TestOptions = allOpt.Test
c.w = cmd.OutOrStdout()
return nil
}
func (c *cleaner) clean() error {
cli, err := docker.NewCompose(c.w)
if err != nil {
return errors.Wrap(err, "failed to build the docker compose client")
}
err = cli.Down(context.TODO(), c.Name)
if err != nil {
return errors.Wrapf(err, "failed to stop the test environment %q", c.Name)
}
fmt.Fprintf(c.w, "Stopped the test environment %q\n", c.Name)
source, err := utils.GetAbsolutePath(c.TestPath)
if err != nil {
return errors.Wrapf(err, "invalid test configuration source %q", c.TestPath)
}
err = os.RemoveAll(source)
if err != nil {
return errors.Wrapf(err, "failed to remove the test configuration source %q", source)
}
fmt.Fprintf(c.w, "Removed the source %q\n", source)
return nil
}

View File

@@ -0,0 +1,175 @@
// 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 test
import (
"encoding/json"
"fmt"
"io"
"os"
"strings"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/config"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/option"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/types"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/utils"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"gopkg.in/yaml.v3"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
)
type creator struct {
optionFile string
option.TestOptions
w io.Writer
}
func newCreateCommand() *cobra.Command {
var c creator
v := viper.New()
createCmd := &cobra.Command{
Use: "create",
Aliases: []string{"c"},
Short: "Create the test environment",
Example: ` # If the option.yaml file exists in the current path, do the following:
hgctl plugin test create
# Explicitly specify the source of the parameters (directory of the build
products) and the directory where the test configuration files is stored
hgctl plugin test create -d ./out -t ./test
`,
PreRun: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(c.config(v, cmd))
},
Run: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(c.create())
},
}
flags := createCmd.PersistentFlags()
option.AddOptionFileFlag(&c.optionFile, flags)
v.BindPFlags(flags)
flags.StringP("from-path", "d", "./out", "Path of storing the build products")
v.BindPFlag("test.from-path", flags.Lookup("from-path"))
v.SetDefault("test.from-path", "./out")
flags.StringP("test-path", "t", "./test", "Path for storing the test configuration")
v.BindPFlag("test.test-path", flags.Lookup("test-path"))
v.SetDefault("test.test-path", "./test")
return createCmd
}
func (c *creator) config(v *viper.Viper, cmd *cobra.Command) error {
allOpt, err := option.ParseOptions(c.optionFile, v, cmd.PersistentFlags())
if err != nil {
return err
}
c.TestOptions = allOpt.Test
c.w = cmd.OutOrStdout()
return nil
}
func (c *creator) create() (err error) {
source, err := utils.GetAbsolutePath(c.FromPath)
if err != nil {
return errors.Wrapf(err, "invalid build products path %q", c.FromPath)
}
c.FromPath = source
target, err := utils.GetAbsolutePath(c.TestPath)
if err != nil {
return errors.Wrapf(err, "invalid test path %q", c.TestPath)
}
c.TestPath = target
fields := testTmplFields{}
// 1. extract the parameters from spec.yaml and convert them to PluginConf
path := fmt.Sprintf("%s/spec.yaml", c.FromPath)
spec, err := types.ParseSpecYAML(path)
if err != nil {
return errors.Wrapf(err, "failed to parse %s", path)
}
fields.PluginConf, err = config.ExtractPluginConfFrom(spec, "", "")
if err != nil {
return errors.Wrapf(err, "failed to get the parameters of plugin-conf.yaml from %s", path)
}
// 2. get DockerCompose instance
fields.DockerCompose = &DockerCompose{
TestPath: c.TestPath,
ProductPath: c.FromPath,
}
// 3. get Envoy instance
var obj interface{}
conf := spec.GetConfigExample()
err = yaml.Unmarshal([]byte(conf), &obj)
if err != nil {
return errors.Wrap(err, "failed to get the example of wasm plugin")
}
b, err := json.MarshalIndent(obj, "", strings.Repeat(" ", 2))
if err != nil {
return errors.Wrap(err, "failed to marshal example to json")
}
jsExample := utils.AddIndent(string(b), strings.Repeat(" ", 30))
fields.Envoy = &Envoy{JSONExample: jsExample}
// 4. generate corresponding test files
if err = os.MkdirAll(target, 0755); err != nil {
return errors.Wrap(err, "failed to create the test environment")
}
if err = c.genTestConfFiles(fields); err != nil {
return errors.Wrap(err, "failed to create the test environment")
}
fmt.Fprintf(c.w, "Created the test environment in %q\n", target)
return nil
}
type testTmplFields struct {
PluginConf *config.PluginConf // for plugin-conf.yaml
DockerCompose *DockerCompose // for docker-compose.yaml
Envoy *Envoy // for envoy.yaml
}
func (c *creator) genTestConfFiles(fields testTmplFields) (err error) {
if err = config.GenPluginConfYAML(fields.PluginConf, c.TestPath); err != nil {
return err
}
if err = genDockerComposeYAML(fields.DockerCompose, c.TestPath); err != nil {
return err
}
if err = genEnvoyYAML(fields.Envoy, c.TestPath); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,64 @@
// 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 test
import (
"context"
"fmt"
"io"
"github.com/alibaba/higress/pkg/cmd/hgctl/docker"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/printers"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
)
func newLsCommand() *cobra.Command {
lsCmd := &cobra.Command{
Use: "ls",
Aliases: []string{"l"},
Short: "List all test environments",
Example: ` hgctl plugin test ls`,
Run: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(runLs(cmd.OutOrStdout()))
},
}
return lsCmd
}
func runLs(w io.Writer) error {
cli, err := docker.NewCompose(w)
if err != nil {
return errors.Wrap(err, "failed to build the docker compose client")
}
list, err := cli.List(context.TODO())
if err != nil {
return errors.Wrap(err, "failed to list all test environments")
}
printer := printers.GetNewTabWriter(w)
// fmt.Fprintf(printer, "NAME\tSTATUS\tCONFIG FILES\n") // compose v2.3.0+
fmt.Fprintf(printer, "NAME\tSTATUS\n")
for _, stack := range list {
fmt.Fprintf(printer, "%s\t%s\n", stack.Name, stack.Status)
}
printer.Flush()
return nil
}

View File

@@ -0,0 +1,115 @@
// 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 test
import (
"context"
"fmt"
"io"
"github.com/alibaba/higress/pkg/cmd/hgctl/docker"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/option"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
)
// TODO(WeixinX): If no test environment exists, create one first and then start
type starter struct {
optionFile string
option.TestOptions
w io.Writer
}
func newStartCommand() *cobra.Command {
var s starter
v := viper.New()
startCmd := &cobra.Command{
Use: "start",
Aliases: []string{"s"},
Short: "Start the test environment",
Example: ` # If the option.yaml file exists in the current path, do the following:
hgctl plugin test start
# Run containers in the background with the option --detach(-d)
hgctl plugin test start -d
`,
PreRun: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(s.config(v, cmd))
},
Run: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(s.start())
},
}
flags := startCmd.PersistentFlags()
option.AddOptionFileFlag(&s.optionFile, flags)
v.BindPFlags(flags)
flags.StringP("name", "p", "wasm-test", "Test environment name")
v.BindPFlag("test.name", flags.Lookup("name"))
v.SetDefault("test.name", "wasm-test")
flags.StringP("test-path", "t", "./test", "Test configuration source")
v.BindPFlag("test.test-path", flags.Lookup("test-path"))
v.SetDefault("test.test-path", "./test")
flags.StringP("compose-file", "c", "", "Docker compose configuration file")
v.BindPFlag("test.compose-file", flags.Lookup("compose-file"))
v.SetDefault("test.compose-file", "")
flags.BoolP("detach", "d", false, "Detached mode: Run containers in the background")
v.BindPFlag("test.detach", flags.Lookup("detach"))
v.SetDefault("test.detach", false)
return startCmd
}
func (s *starter) config(v *viper.Viper, cmd *cobra.Command) error {
allOpt, err := option.ParseOptions(s.optionFile, v, cmd.PersistentFlags())
if err != nil {
return err
}
s.TestOptions = allOpt.Test
s.w = cmd.OutOrStdout()
return nil
}
func (s *starter) start() error {
cli, err := docker.NewCompose(s.w)
if err != nil {
return errors.Wrap(err, "failed to build the docker compose client")
}
var configs []string
if s.ComposeFile != "" {
configs = []string{s.ComposeFile}
}
err = cli.Up(context.TODO(), s.Name, configs, s.TestPath, s.Detach)
if err != nil {
return errors.Wrap(err, "failed to start the test environment")
}
fmt.Fprintf(s.w, "Started the test environment %q\n", s.Name)
return nil
}

View File

@@ -0,0 +1,95 @@
// 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 test
import (
"context"
"fmt"
"io"
"github.com/alibaba/higress/pkg/cmd/hgctl/docker"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/option"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
)
type stopper struct {
optionFile string
option.TestOptions
w io.Writer
}
func newStopCommand() *cobra.Command {
var s stopper
v := viper.New()
stopCmd := &cobra.Command{
Use: "stop",
Aliases: []string{"st"},
Short: "Stop the test environment",
Example: ` # Stop responding to the compose containers with the option --name(-p)
hgctl plugin test stop -p wasm-test
`,
PreRun: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(s.config(v, cmd))
},
Run: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(s.stop())
},
}
flags := stopCmd.PersistentFlags()
option.AddOptionFileFlag(&s.optionFile, flags)
v.BindPFlags(flags)
flags.StringP("name", "p", "wasm-test", "Test environment name")
v.BindPFlag("test.name", flags.Lookup("name"))
v.SetDefault("test.name", "wasm-test")
return stopCmd
}
func (s *stopper) config(v *viper.Viper, cmd *cobra.Command) error {
allOpt, err := option.ParseOptions(s.optionFile, v, cmd.PersistentFlags())
if err != nil {
return err
}
s.TestOptions = allOpt.Test
s.w = cmd.OutOrStdout()
return nil
}
func (s *stopper) stop() error {
cli, err := docker.NewCompose(s.w)
if err != nil {
return errors.Wrap(err, "failed to build the docker compose client")
}
err = cli.Down(context.TODO(), s.Name)
if err != nil {
return errors.Wrapf(err, "failed to stop the test environment %q", s.Name)
}
fmt.Fprintf(s.w, "Stopped the test environment %q\n", s.Name)
return nil
}

View File

@@ -0,0 +1,167 @@
// 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 test
import (
"fmt"
"os"
"text/template"
)
const (
dockerComposeYAML = `# File generated by hgctl. Modify as required.
version: '3.7'
services:
envoy:
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/envoy:1.20
command: envoy -c /etc/envoy/envoy.yaml --component-log-level wasm:debug
depends_on:
- httpbin
networks:
- wasmtest
ports:
- "10000:10000"
volumes:
- {{ .TestPath }}/envoy.yaml:/etc/envoy/envoy.yaml
- {{ .ProductPath }}/plugin.wasm:/etc/envoy/plugin.wasm
httpbin:
image: kennethreitz/httpbin:latest
networks:
- wasmtest
ports:
- "12345:80"
networks:
wasmtest: {}
`
envoyYAML = `# File generated by hgctl. Modify as required.
admin:
address:
socket_address:
protocol: TCP
address: 0.0.0.0
port_value: 9901
static_resources:
listeners:
- name: listener_0
address:
socket_address:
protocol: TCP
address: 0.0.0.0
port_value: 10000
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
scheme_header_transformation:
scheme_to_overwrite: https
stat_prefix: ingress_http
# Output envoy logs to stdout
access_log:
- name: envoy.access_loggers.stdout
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
# Modify as required
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: httpbin
http_filters:
- name: wasmtest
typed_config:
"@type": type.googleapis.com/udpa.type.v1.TypedStruct
type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
value:
config:
name: wasmtest
vm_config:
runtime: envoy.wasm.runtime.v8
code:
local:
filename: /etc/envoy/plugin.wasm
configuration:
"@type": "type.googleapis.com/google.protobuf.StringValue"
# Modify as required
value: |
{{ .JSONExample }}
- name: envoy.filters.http.router
clusters:
- name: httpbin
connect_timeout: 30s
type: LOGICAL_DNS
# Comment out the following line to test on v6 networks
dns_lookup_family: V4_ONLY
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: httpbin
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: httpbin
port_value: 80
`
)
type DockerCompose struct {
TestPath string
ProductPath string
}
type Envoy struct {
JSONExample string
}
func genDockerComposeYAML(d *DockerCompose, dir string) error {
path := fmt.Sprintf("%s/docker-compose.yaml", dir)
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
if err = template.Must(template.New("DockerComposeYAML").Parse(dockerComposeYAML)).Execute(f, d); err != nil {
return err
}
return nil
}
func genEnvoyYAML(e *Envoy, dir string) error {
path := fmt.Sprintf("%s/envoy.yaml", dir)
f, err := os.Create(path)
if err != nil {
panic(fmt.Sprintf("failed to create %q: %v\n", path, err))
}
defer f.Close()
if err = template.Must(template.New("EnvoyYAML").Parse(envoyYAML)).Execute(f, e); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,35 @@
// 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 test
import (
"github.com/spf13/cobra"
)
func NewCommand() *cobra.Command {
testCmd := &cobra.Command{
Use: "test",
Aliases: []string{"t"},
Short: "Test WASM plugin locally",
}
testCmd.AddCommand(newCreateCommand())
testCmd.AddCommand(newStartCommand())
testCmd.AddCommand(newStopCommand())
testCmd.AddCommand(newCleanCommand())
testCmd.AddCommand(newLsCommand())
return testCmd
}

View File

@@ -0,0 +1,163 @@
// 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 types
import (
"fmt"
"strings"
"github.com/pkg/errors"
)
type Annotation struct {
Type AnnotationType
I18nType I18nType
Text string
}
type AnnotationType int
const (
// Info
ACategory AnnotationType = iota
AName
ATitle
ADescription
AIconUrl
AVersion
AContactName
AContactUrl
AContactEmail
// Spec
APhase
APriority
// Schema
AScope
AExample
AEnd
AUnknown
)
func str2AnnotationType(typ string) AnnotationType {
switch strings.ToLower(typ) {
case "@category":
return ACategory
case "@name":
return AName
case "@title":
return ATitle
case "@description":
return ADescription
case "@iconurl":
return AIconUrl
case "@version":
return AVersion
case "@contact.name":
return AContactName
case "@contact.url":
return AContactUrl
case "@contact.email":
return AContactEmail
case "@phase":
return APhase
case "@priority":
return APriority
case "@scope":
return AScope
case "@example":
return AExample
case "@end":
return AEnd
default:
return AUnknown
}
}
// GetAnnotations returns all annotations in the comment
func GetAnnotations(comment string) []Annotation {
as := make([]Annotation, 0)
cs := strings.Split(comment, "\n")
for i := 0; i < len(cs); i++ {
a, err := getAnnotationFrom(cs[i])
if err != nil {
continue
}
if a.Type == AExample {
for j := i + 1; j < len(cs); j++ {
if str2AnnotationType(strings.TrimSpace(cs[j])) == AEnd {
break
}
if j == i+1 {
a.Text = fmt.Sprintf("%s", cs[j])
} else {
a.Text = fmt.Sprintf("%s\n%s", a.Text, cs[j])
}
}
}
as = append(as, a)
}
return as
}
func getAnnotationFrom(c string) (Annotation, error) {
// the annotation is like `@AnnotationType [I18nType] Text`
c = strings.TrimSpace(c)
if !strings.HasPrefix(c, "@") {
return Annotation{}, errors.New("invalid annotation")
}
// first param: AnnotationType
idx := strings.Index(c, " ")
if idx == -1 && str2AnnotationType(c) == AUnknown { // only an invalid annotation type
return Annotation{}, errors.New("invalid annotation")
}
// idx != -1 or type != unknown
var typ AnnotationType
if idx == -1 {
typ = str2AnnotationType(c)
} else {
typ = str2AnnotationType(strings.TrimSpace(c[0:idx]))
}
c = strings.TrimSpace(c[idx+1:])
a := Annotation{
Type: typ,
I18nType: I18nDefault,
Text: c,
}
if a.Type != ATitle && a.Type != ADescription { // other annotation types do not define i18n
a.I18nType = I18nUndefined
}
if idx == -1 && typ != AUnknown { // only a valid annotation type
a.Text = ""
}
// second or/and third param: I18nType and Text
idx = strings.Index(c, " ")
if idx == -1 {
return a, nil
}
i18n := str2I18nType(strings.TrimSpace(c[0:idx]))
if i18n == I18nUnknown {
return a, nil
}
a.I18nType = i18n
a.Text = strings.TrimSpace(c[idx+1:])
return a, nil
}

View File

@@ -0,0 +1,176 @@
// 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 types
import (
"bytes"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
"k8s.io/apimachinery/pkg/util/json"
)
func (s JSON) MarshalJSON() ([]byte, error) {
if len(s.Raw) > 0 {
var obj interface{}
err := json.Unmarshal(s.Raw, &obj)
if err != nil {
return []byte("null"), err
}
return json.Marshal(obj)
}
return []byte("null"), nil
}
func (s *JSON) UnmarshalJSON(data []byte) error {
if len(data) > 0 && !bytes.Equal(data, []byte("null")) {
s.Raw = data
}
return nil
}
func (s JSON) MarshalYAML() (interface{}, error) {
if len(s.Raw) > 0 {
var obj interface{}
err := yaml.Unmarshal(s.Raw, &obj)
if err != nil {
return "null", err
}
return obj, nil
}
return "null", nil
}
func (s JSONSchemaPropsOrArray) MarshalJSON() ([]byte, error) {
if len(s.JSONSchemas) > 0 {
return json.Marshal(s.JSONSchemas)
}
return json.Marshal(s.Schema)
}
func (s *JSONSchemaPropsOrArray) UnmarshalJSON(data []byte) error {
var nw JSONSchemaPropsOrArray
var first byte
if len(data) > 1 {
first = data[0]
}
if first == '{' {
var sch JSONSchemaProps
if err := json.Unmarshal(data, &sch); err != nil {
return err
}
nw.Schema = &sch
}
if first == '[' {
if err := json.Unmarshal(data, &nw.JSONSchemas); err != nil {
return err
}
}
*s = nw
return nil
}
func (s JSONSchemaPropsOrArray) MarshalYAML() (interface{}, error) {
if len(s.JSONSchemas) > 0 {
return s.JSONSchemas, nil
}
return s.Schema, nil
}
func (s JSONSchemaPropsOrBool) MarshalJSON() ([]byte, error) {
if s.Schema != nil {
return json.Marshal(s.Schema)
}
if s.Schema == nil && !s.Allows {
return []byte("false"), nil
}
return []byte("true"), nil
}
func (s *JSONSchemaPropsOrBool) UnmarshalJSON(data []byte) error {
var nw JSONSchemaPropsOrBool
switch {
case len(data) == 0:
case data[0] == '{':
var sch JSONSchemaProps
if err := json.Unmarshal(data, &sch); err != nil {
return err
}
nw.Allows = true
nw.Schema = &sch
case len(data) == 4 && string(data) == "true":
nw.Allows = true
case len(data) == 5 && string(data) == "false":
nw.Allows = false
default:
return errors.New("boolean or JSON schema expected")
}
*s = nw
return nil
}
func (s JSONSchemaPropsOrBool) MarshalYAML() (interface{}, error) {
if s.Schema != nil {
return yaml.Marshal(s.Schema)
}
if s.Schema == nil && !s.Allows {
return false, nil
}
return true, nil
}
func (s JSONSchemaPropsOrStringArray) MarshalJSON() ([]byte, error) {
if len(s.Property) > 0 {
return json.Marshal(s.Property)
}
if s.Schema != nil {
return json.Marshal(s.Schema)
}
return []byte("null"), nil
}
func (s *JSONSchemaPropsOrStringArray) UnmarshalJSON(data []byte) error {
var first byte
if len(data) > 1 {
first = data[0]
}
var nw JSONSchemaPropsOrStringArray
if first == '{' {
var sch JSONSchemaProps
if err := json.Unmarshal(data, &sch); err != nil {
return err
}
nw.Schema = &sch
}
if first == '[' {
if err := json.Unmarshal(data, &nw.Property); err != nil {
return err
}
}
*s = nw
return nil
}
func (s JSONSchemaPropsOrStringArray) MarshalYAML() (interface{}, error) {
if len(s.Property) > 0 {
return s.Property, nil
}
if s.Schema != nil {
return s.Schema, nil
}
return "null", nil
}

View File

@@ -0,0 +1,393 @@
// 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 types
import (
"fmt"
"os"
"sort"
"strconv"
"strings"
"github.com/iancoleman/orderedmap"
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
)
// WasmPluginMeta is used to describe WASM plugin metadata,
// see https://higress.io/en-us/docs/user/wasm-image-spec/
type WasmPluginMeta struct {
APIVersion string `json:"apiVersion" yaml:"apiVersion"`
Info WasmPluginInfo `json:"info" yaml:"info"`
Spec WasmPluginSpec `json:"spec" yaml:"spec"`
}
func defaultWsamPluginMeta() *WasmPluginMeta {
return &WasmPluginMeta{
APIVersion: "1.0.0",
Info: WasmPluginInfo{
Category: CategoryCustom,
Name: "Unnamed",
XTitleI18n: make(map[I18nType]string),
XDescriptionI18n: make(map[I18nType]string),
Version: "0.1.0",
},
Spec: WasmPluginSpec{
Phase: PhaseUnspecified,
Priority: 0,
},
}
}
// ParseSpecYAML parses the `spec.yaml` to WasmPluginMeta
func ParseSpecYAML(spec string) (*WasmPluginMeta, error) {
f, err := os.Open(spec)
if err != nil {
return nil, err
}
defer f.Close()
var m WasmPluginMeta
dc := k8syaml.NewYAMLOrJSONDecoder(f, 4096)
if err = dc.Decode(&m); err != nil {
return nil, err
}
return &m, nil
}
// ParseGoSrc parses the config model of the golang WASM plugin project to WasmPluginMeta
func ParseGoSrc(dir, model string) (*WasmPluginMeta, error) {
mp, err := NewModelParser(dir)
if err != nil {
return nil, err
}
m, err := mp.GetModel(model)
if err != nil {
return nil, err
}
meta := defaultWsamPluginMeta()
meta.setByConfigModel(m)
return meta, nil
}
func (meta *WasmPluginMeta) setByConfigModel(model *Model) {
_, schema := recursiveSetSchema(model, nil)
meta.Spec.ConfigSchema.OpenAPIV3Schema = schema
meta.setModelAnnotations(model.Doc)
}
func recursiveSetSchema(model *Model, parent *JSONSchemaProps) (string, *JSONSchemaProps) {
cur := NewJSONSchemaProps()
cur.Type = model.Type
if parent != nil {
cur.HandleFieldAnnotations(model.Doc)
}
newName := cur.HandleFieldTags(model.Tag, parent, model.Name)
if IsArray(model.Type) {
item := NewJSONSchemaProps()
item.Type = GetItemType(cur.Type)
cur.Type = "array"
if IsObject(item.Type) {
item.Properties = make(map[string]JSONSchemaProps)
for _, field := range model.Fields {
name, child := recursiveSetSchema(&field, cur)
item.Properties[name] = *child
}
}
cur.Items = &JSONSchemaPropsOrArray{Schema: item}
} else if IsObject(model.Type) { // type may be `array of object`, and it is handled in the first branch
for _, field := range model.Fields {
name, child := recursiveSetSchema(&field, cur)
cur.Properties[name] = *child
}
}
return newName, cur
}
func (meta *WasmPluginMeta) setModelAnnotations(comment string) {
as := GetAnnotations(comment)
for _, a := range as {
switch a.Type {
// Info
case ACategory:
meta.Info.Category = Category(a.Text)
case AName:
meta.Info.Name = a.Text
case ATitle:
if meta.Info.Title == "" {
meta.Info.Title = a.Text
}
meta.Info.XTitleI18n[a.I18nType] = a.Text
case ADescription:
if meta.Info.Description == "" {
meta.Info.Description = a.Text
}
meta.Info.XDescriptionI18n[a.I18nType] = a.Text
case AIconUrl:
meta.Info.IconUrl = a.Text
case AVersion:
meta.Info.Version = a.Text
case AContactName:
meta.Info.Contact.Name = a.Text
case AContactUrl:
meta.Info.Contact.Url = a.Text
case AContactEmail:
meta.Info.Contact.Email = a.Text
// Spec
case APhase:
meta.Spec.Phase = Phase(a.Text)
case APriority:
priority, err := strconv.ParseInt(a.Text, 10, 64)
if err != nil {
priority = 0
}
meta.Spec.Priority = priority
// Schema
case AExample:
meta.Spec.ConfigSchema.OpenAPIV3Schema.Example = &JSON{Raw: []byte(a.Text)}
case AScope:
meta.Spec.ConfigSchema.OpenAPIV3Schema.Scope = Scope(a.Text)
}
}
}
type WasmPluginInfo struct {
Category Category `json:"category" yaml:"category"`
Name string `json:"name" yaml:"name"`
Title string `json:"title,omitempty" yaml:"title,omitempty"`
XTitleI18n map[I18nType]string `json:"x-title-i18n,omitempty" yaml:"x-title-i18n,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
XDescriptionI18n map[I18nType]string `json:"x-description-i18n,omitempty" yaml:"x-description-i18n,omitempty"`
IconUrl string `json:"iconUrl,omitempty" yaml:"iconUrl,omitempty"`
Version string `json:"version" yaml:"version"`
Contact Contact `json:"contact,omitempty" yaml:"contact,omitempty"`
}
type Category string
const (
CategoryAuth Category = "auth"
CategorySecurity Category = "security"
CategoryProtocol Category = "protocol"
CategoryFlowControl Category = "flow-control"
CategoryFlowMonitor Category = "flow-monitor"
CategoryCustom Category = "custom"
CategoryDefault = CategoryCustom
)
const (
IconAuth = "https://img.alicdn.com/imgextra/i4/O1CN01BPFGlT1pGZ2VDLgaH_!!6000000005333-2-tps-42-42.png"
IconSecurity = "https://img.alicdn.com/imgextra/i1/O1CN01jKT9vC1O059vNaq5u_!!6000000001642-2-tps-42-42.png"
IconProtocol = "https://img.alicdn.com/imgextra/i2/O1CN01xIywow1mVGuRUjbhe_!!6000000004959-2-tps-42-42.png"
IconFlowControl = "https://img.alicdn.com/imgextra/i3/O1CN01bAFa9k1t1gdQcVTH0_!!6000000005842-2-tps-42-42.png"
IconFlowMonitor = "https://img.alicdn.com/imgextra/i4/O1CN01aet3s61MoLOEEhRIo_!!6000000001481-2-tps-42-42.png"
IconCustom = "https://img.alicdn.com/imgextra/i1/O1CN018iKKih1iVx287RltL_!!6000000004419-2-tps-42-42.png"
IconDefault = IconCustom
)
func Category2IconUrl(category Category) string {
switch category {
case CategoryAuth:
return IconAuth
case CategorySecurity:
return IconSecurity
case CategoryProtocol:
return IconProtocol
case CategoryFlowControl:
return IconFlowControl
case CategoryFlowMonitor:
return IconFlowMonitor
case CategoryCustom:
return IconCustom
default:
return IconDefault
}
}
type I18nType string
const (
I18nZH_CN I18nType = "zh-CN" // default
I18nEN_US I18nType = "en-US"
I18nUndefined I18nType = "undefined" // i18n type is empty in the annotation
I18nUnknown I18nType = "unknown"
I18nDefault = I18nEN_US
)
func str2I18nType(typ string) I18nType {
switch strings.ToLower(typ) {
case "zh-cn":
return I18nZH_CN
case "en-us":
return I18nEN_US
default:
return I18nUnknown
}
}
type Contact struct {
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Url string `json:"url,omitempty" yaml:"url,omitempty"`
Email string `json:"email,omitempty" yaml:"email,omitempty"`
}
type WasmPluginSpec struct {
// Phase refers to https://istio.io/latest/docs/reference/config/proxy_extensions/wasm-plugin/#PluginPhase
Phase Phase `json:"phase" yaml:"phase"`
// Priority refers to https://istio.io/latest/docs/reference/config/proxy_extensions/wasm-plugin/#WasmPlugin
Priority int64 `json:"priority" yaml:"priority"`
ConfigSchema ConfigSchema `json:"configSchema" yaml:"configSchema"`
}
type Phase string
const (
PhaseUnspecified Phase = "UNSPECIFIED_PHASE"
PhaseAuthn Phase = "AUTHN"
PhaseAuthz Phase = "AUTHZ"
PhaseStats Phase = "STATS"
PhaseDefault = PhaseUnspecified
)
type ConfigSchema struct {
OpenAPIV3Schema *JSONSchemaProps `json:"openAPIV3Schema" yaml:"openAPIV3Schema"`
}
// GetConfigExample returns a pretty WASM plugin config example
func (meta *WasmPluginMeta) GetConfigExample() string {
s := meta.Spec.ConfigSchema.OpenAPIV3Schema
if s != nil {
return s.GetExample()
}
return ""
}
// getLanguageUnionOrderMap returns a ordered map of language union of title and description.
// If there is a language type in title that description does not have, the value is "No description"
func (meta *WasmPluginMeta) getLanguageUnionOrderMap() *orderedmap.OrderedMap {
m := orderedmap.New()
for i18n, desc := range meta.Info.XDescriptionI18n {
m.Set(string(i18n), desc)
}
for i18n := range meta.Info.XTitleI18n {
if _, ok := m.Get(string(i18n)); !ok {
m.Set(string(i18n), "No description")
}
}
if len(m.Keys()) == 0 {
m.Set(string(I18nEN_US), "No description")
}
m.SortKeys(sort.Strings)
return m
}
// WasmUsage is used to describe WASM plugin usage in the Markdown document
type WasmUsage struct {
I18nType I18nType
Description string
ConfigEntries []ConfigEntry
Example string
}
type ConfigEntry struct {
Name string
Type string
Requirement string
Default string
Description string
}
// GetUsages returns WASM plugin usages in different languages
func (meta *WasmPluginMeta) GetUsages() ([]WasmUsage, error) {
usages := make([]WasmUsage, 0)
example := meta.GetConfigExample()
m := meta.getLanguageUnionOrderMap()
for _, i18n := range m.Keys() {
desc, ok := m.Get(i18n)
if !ok {
continue
}
u := WasmUsage{
I18nType: I18nType(i18n),
Description: desc.(string),
ConfigEntries: make([]ConfigEntry, 0),
Example: example,
}
getConfigEntries(meta.Spec.ConfigSchema.OpenAPIV3Schema, &u.ConfigEntries, I18nType(i18n))
usages = append(usages, u)
}
return usages, nil
}
func getConfigEntries(schema *JSONSchemaProps, entries *[]ConfigEntry, i18n I18nType) {
doGetConfigEntries(schema, entries, "", "", i18n, false)
}
func doGetConfigEntries(schema *JSONSchemaProps, entries *[]ConfigEntry, parentName, name string, i18n I18nType, required bool) {
newName := constructName(parentName, name)
switch schema.Type {
case "object":
m := schema.GetPropertiesOrderMap()
for _, fieldName := range m.Keys() {
val, ok := m.Get(fieldName)
if !ok {
continue
}
props := val.(JSONSchemaProps)
required = schema.IsRequired(fieldName)
doGetConfigEntries(&props, entries, newName, fieldName, i18n, required)
}
case "array":
itemType := schema.Items.Schema.Type
e := ConfigEntry{
Name: newName,
Type: ArrayPrefix + itemType,
Requirement: schema.JoinRequirementsBy(i18n, required),
Default: schema.GetDefaultValue(),
Description: schema.XDescriptionI18n[i18n],
}
*entries = append(*entries, e)
if itemType == "object" {
doGetConfigEntries(schema.Items.Schema, entries, newName+"[*]", "", i18n, false)
}
default:
e := ConfigEntry{
Name: newName,
Type: schema.Type,
Requirement: schema.JoinRequirementsBy(i18n, required),
Default: schema.GetDefaultValue(),
Description: schema.XDescriptionI18n[i18n],
}
*entries = append(*entries, e)
}
}
func constructName(parent, name string) string {
newName := name
if parent != "" {
if name != "" {
newName = fmt.Sprintf("%s.%s", parent, name)
} else {
newName = parent
}
}
return newName
}

View File

@@ -0,0 +1,391 @@
// 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 types
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/fatih/structtag"
"github.com/pkg/errors"
)
const (
ArrayPrefix = "array of "
ObjectSuffix = "object"
)
// IsArray returns true if the given type is an `array of <type>`
func IsArray(typ string) bool {
return strings.HasPrefix(typ, ArrayPrefix)
}
// GetItemType returns the item type of array, e.g.: array of int -> int
func GetItemType(typ string) string {
return strings.TrimPrefix(typ, ArrayPrefix)
}
// IsObject returns true if the given type is an `object` or an `array of object`
func IsObject(typ string) bool {
return strings.HasSuffix(typ, ObjectSuffix)
}
var (
ErrInvalidModel = errors.New("invalid model")
ErrInvalidFiledType = errors.New("invalid field type")
)
type ModelParser struct {
structs map[string]*astNode
// alias for a basic type, such as type MyInt int: MyInt -> int
// TODO(WeixinX): Support alias for package name
alias map[string]*astNode
}
type Model struct {
Name string
Type string
Doc string
Tag string
Fields []Model
}
type astNode struct {
name string
doc string
expr ast.Expr
}
func (m *Model) Inspect(f func(model *Model) bool) {
ctn := f(m)
if !ctn {
return
}
for _, field := range m.Fields {
field.Inspect(f)
}
}
// NewModelParser new a model parser based on the dir where the given model exists
func NewModelParser(dir string) (*ModelParser, error) {
pkgs, err := walkGoSrc(dir)
if err != nil {
return nil, err
}
p := &ModelParser{
structs: make(map[string]*astNode),
alias: make(map[string]*astNode),
}
for _, pkg := range pkgs {
for _, f := range pkg.Files {
for _, decl := range f.Decls {
x, ok := decl.(*ast.GenDecl)
if !ok || x.Tok != token.TYPE {
continue
}
for _, spec := range x.Specs {
ts, ok := spec.(*ast.TypeSpec)
if !ok {
continue
}
switch t := ts.Type.(type) {
case *ast.StructType:
if !t.Struct.IsValid() {
continue
}
s := &astNode{
name: ts.Name.String(),
expr: t,
}
if pkg.Name != "main" { // ignore main package prefix
s.name = fmt.Sprintf("%s.%s", pkg.Name, s.name)
}
if x.Doc != nil {
s.doc = x.Doc.Text()
}
p.structs[s.name] = s
case *ast.InterfaceType:
continue
default: // for alias, such as `type MyInt int`
alias := ts.Name.String()
if pkg.Name != "main" {
alias = fmt.Sprintf("%s.%s", pkg.Name, alias)
}
name, err := p.getModelName(t)
if err != nil {
continue
}
p.alias[alias] = &astNode{
name: name,
expr: t,
}
}
}
}
}
}
// gets the true type (ast node) of the alias
for alias := range p.alias {
n := p.recursiveAlias(alias)
if n != nil {
p.alias[alias] = n
}
}
return p, nil
}
func walkGoSrc(dir string) (map[string]*ast.Package, error) {
info, err := os.Stat(dir)
if err != nil {
return nil, err
}
if !info.IsDir() {
return nil, errors.Errorf("%q is not a directory", dir)
}
fset := token.NewFileSet()
pkgs := make(map[string]*ast.Package)
walk := func(path string, info fs.FileInfo, err error) error {
if !info.IsDir() {
return nil
}
tmp, err := parser.ParseDir(fset, path, nil, parser.ParseComments)
if err != nil {
return err
}
for k, v := range tmp {
pkgs[k] = v
}
return nil
}
if err := filepath.Walk(dir, walk); err != nil {
return nil, errors.Wrapf(err, "failed to walk path %q", dir)
}
return pkgs, nil
}
func (p *ModelParser) recursiveAlias(alias string) *astNode {
if s, ok := p.structs[alias]; ok {
return s
}
if n, ok := p.alias[alias]; ok {
if n.name != alias {
ret := p.recursiveAlias(n.name)
if ret != nil {
return ret
}
}
return n
}
return nil
}
// GetModel return the specified model
func (p *ModelParser) GetModel(model string) (*Model, error) {
fields, err := p.parseModelFields(model)
if err != nil {
return nil, err
}
m := &Model{
Name: model,
Type: "object",
Fields: fields,
}
m.setDoc(p.structs[model].doc)
return m, nil
}
func (p *ModelParser) parseModelFields(model string) (fields []Model, err error) {
var s *astNode
if _, ok := p.structs[model]; ok {
s = p.structs[model]
} else if _, ok = p.alias[model]; ok {
s = p.alias[model]
} else {
return nil, ErrInvalidModel
}
st, ok := s.expr.(*ast.StructType)
if !ok || st.Fields == nil {
return nil, ErrInvalidModel
}
pkgName := ""
if idx := strings.Index(model, "."); idx != -1 {
pkgName = model[:idx+1] // pkgName includes "."
}
for _, field := range st.Fields.List {
if skipField(field) {
continue
}
fd := Model{Name: field.Names[0].String()}
if field.Doc != nil {
fd.setDoc(field.Doc.Text())
}
if field.Tag != nil {
ignore, err := fd.setTag(field.Tag.Value)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse tag %q of the field %q", fd.Tag, fd.Name)
}
if ignore {
continue
}
}
fd.Type, err = p.parseFiledType(pkgName, field.Type)
if err != nil {
return nil, err
}
if IsObject(fd.Type) {
subModel, err := p.getModelName(field.Type)
if err != nil {
return nil, err
}
fd.Fields, err = p.parseModelFields(subModel)
if err != nil {
return nil, err
}
}
fields = append(fields, fd)
}
return fields, nil
}
func skipField(field *ast.Field) bool {
name := field.Names
return field == nil || name == nil || len(name) < 1 || name[0] == nil || name[0].String() == "_"
}
func (m *Model) setDoc(str string) {
m.Doc = strings.TrimSpace(str)
}
func (m *Model) setTag(str string) (bool, error) {
str = strings.Trim(str, "` ")
if str == "" {
return false, nil
}
ignore := false
tag, err := structtag.Parse(str)
if err != nil {
return false, err
}
m.Tag = str
val, err := tag.Get("yaml")
if err == nil {
if val.Name == "-" || val.Name == "" {
ignore = true
}
}
return ignore, nil
}
func (p *ModelParser) getModelName(typ ast.Expr) (string, error) {
return p.doGetModelName("", typ)
}
func (p *ModelParser) doGetModelName(pkgName string, typ ast.Expr) (string, error) {
switch t := typ.(type) {
case *ast.StarExpr: // *int -> int
return p.doGetModelName(pkgName, t.X)
case *ast.ArrayType: // slice or array
return p.doGetModelName(pkgName, t.Elt)
case *ast.SelectorExpr: // <pkg_name>.<field_name>
pkg, ok := t.X.(*ast.Ident)
if !ok {
return "", ErrInvalidFiledType
}
pName := pkg.Name + "."
return p.doGetModelName(pName, t.Sel)
case *ast.Ident:
return pkgName + t.Name, nil
default:
return "", ErrInvalidFiledType
}
}
func (p *ModelParser) parseFiledType(pkgName string, typ ast.Expr) (string, error) {
switch t := typ.(type) {
case *ast.StructType: // nested struct
return string(JsonTypeObject), nil
case *ast.StarExpr: // *int -> int
return p.parseFiledType(pkgName, t.X)
case *ast.ArrayType: // slice or array
ret, err := p.parseFiledType(pkgName, t.Elt)
if err != nil {
return "", err
}
return ArrayPrefix + ret, nil
case *ast.SelectorExpr: // <pkg_name>.<field_name>
pkg, ok := t.X.(*ast.Ident)
if !ok {
return "", ErrInvalidFiledType
}
pName := pkg.Name + "."
return p.parseFiledType(pName, t.Sel)
case *ast.Ident:
fName := pkgName + t.Name
if _, ok := p.structs[fName]; ok {
return string(JsonTypeObject), nil
}
if alias, ok := p.alias[fName]; ok {
return p.parseFiledType(pkgName, alias.expr)
}
jsonType, err := convert2JsonType(t.Name)
return string(jsonType), err
default:
return "", ErrInvalidFiledType
}
}
func convert2JsonType(typ string) (JsonType, error) {
switch typ {
case "int", "int8", "int16", "int32", "int64",
"uint", "uint8", "uint16", "uint32", "uint64":
return JsonTypeInteger, nil
case "float32", "float64":
return JsonTypeNumber, nil
case "bool":
return JsonTypeBoolean, nil
case "string":
return JsonTypeString, nil
case "struct":
return JsonTypeObject, nil
default:
return "", ErrInvalidFiledType
}
}
type JsonType string
const (
JsonTypeInteger JsonType = "integer"
JsonTypeNumber JsonType = "number"
JsonTypeBoolean JsonType = "boolean"
JsonTypeString JsonType = "string"
JsonTypeObject JsonType = "object"
JsonTypeArray JsonType = "array"
)

View File

@@ -0,0 +1,379 @@
// 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 types
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestGetModel(t *testing.T) {
var (
BasicStructField = []Model{
{
Name: "Name",
Type: "string",
},
{
Name: "Age",
Type: "integer",
},
{
Name: "Married",
Type: "boolean",
},
{
Name: "Salary",
Type: "number",
},
}
ExternalStructField = []Model{
{
Name: "one",
Type: "string",
},
{
Name: "two",
Type: "integer",
},
{
Name: "three",
Type: "array of boolean",
},
}
NestedStructField = []Model{
{
Name: "Simple",
Type: "string",
},
{
Name: "Complex",
Type: "array of integer",
},
}
)
cases := []struct {
name string
expected *Model
errMsg string
}{
{
name: "TestBasicStruct",
expected: &Model{
Name: "TestBasicStruct",
Type: "object",
Fields: BasicStructField,
},
},
{
name: "TestComplexStruct",
expected: &Model{
Name: "TestComplexStruct",
Type: "object",
Fields: []Model{
{
Name: "Array",
Type: "array of integer",
},
{
Name: "Slice",
Type: "array of string",
},
{
Name: "Pointer",
Type: "string",
},
{
Name: "PPPointer",
Type: "boolean",
},
{
Name: "ArrayPointer",
Type: "array of integer",
},
{
Name: "SlicePointer",
Type: "array of integer",
},
{
Name: "StructPointerSlice",
Type: "array of object",
Fields: BasicStructField,
},
{
Name: "StructArrayPointer",
Type: "array of object",
Fields: BasicStructField,
},
},
},
},
{
name: "TestAliasStruct",
expected: &Model{
Name: "TestAliasStruct",
Type: "object",
Fields: []Model{
{
Name: "MyString",
Type: "string",
},
{
Name: "MyPointerInt",
Type: "integer",
},
{
Name: "MyStruct",
Type: "object",
Fields: BasicStructField,
},
},
},
},
{
name: "TestExternalStruct",
expected: &Model{
Name: "TestExternalStruct",
Type: "object",
Fields: []Model{
{
Name: "InternalFloat",
Type: "number",
},
{
Name: "ExStruct",
Type: "object",
Fields: ExternalStructField,
},
{
Name: "ExternalInt",
Type: "integer",
},
{
Name: "ExBool",
Type: "boolean",
},
{
Name: "ExSlice",
Type: "array of string",
},
},
},
},
{
name: "TestNestedStruct",
expected: &Model{
Name: "TestNestedStruct",
Type: "object",
Fields: []Model{
{
Name: "NestedStruct",
Type: "object",
Fields: []Model{
{
Name: "NestedStruct",
Type: "object",
Fields: NestedStructField,
},
{
Name: "NestedInt",
Type: "integer",
},
{
Name: "NestedString",
Type: "string",
},
},
},
},
},
},
{
name: "ext.TestExStruct",
expected: &Model{
Name: "ext.TestExStruct",
Type: "object",
Fields: ExternalStructField,
},
},
{
name: "ext.TestNestedStruct",
expected: &Model{
Name: "ext.TestNestedStruct",
Type: "object",
Fields: []Model{
{
Name: "NestedStruct",
Type: "object",
Fields: NestedStructField,
},
{
Name: "NestedInt",
Type: "integer",
},
{
Name: "NestedString",
Type: "string",
},
},
},
},
}
p, err := NewModelParser("./testdata/types")
require.NoError(t, err)
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
actual, err := p.GetModel(c.name)
if c.errMsg != "" {
require.EqualError(t, err, c.errMsg)
} else {
require.NoError(t, err)
require.Equal(t, c.expected, actual)
}
})
}
}
func TestParseStructAndAlias(t *testing.T) {
cases := []struct {
name string
dir string
expectedStructs map[string]struct{}
expectedAlias map[string]string
}{
{
name: "Basic",
dir: "./testdata/types",
expectedStructs: map[string]struct{}{
"TestBasicStruct": {},
"TestComplexStruct": {},
"TestAliasStruct": {},
"TestExternalStruct": {},
"TestNestedStruct": {},
"ext.TestExStruct": {},
"ext.TestNestedStruct": {},
"nested.TestNestedStruct": {},
},
expectedAlias: map[string]string{
"MyString": "string",
"MyPointerInt": "int",
"MyStruct": "TestBasicStruct",
"NestedAlias": "nested.TestNestedStruct",
"NestedBasicAlias": "bool",
"ext.ExAlias": "nested.TestNestedStruct",
"ext.ExPointerInt": "int",
"ext.ExBool": "bool",
"ext.ExSlice": "string",
"nested.NestedInt": "int",
"nested.NestedString": "string",
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
p, err := NewModelParser(c.dir)
require.NoError(t, err)
actualStructs := make(map[string]struct{})
for _, s := range p.structs {
actualStructs[s.name] = struct{}{}
}
require.Equal(t, c.expectedStructs, actualStructs)
actualAlias := make(map[string]string)
for name, alias := range p.alias {
actualAlias[name] = alias.name
}
require.Equal(t, c.expectedAlias, actualAlias)
})
}
}
func TestStructFieldDocAndTag(t *testing.T) {
var BasicStructField = []Model{
{
Name: "Name",
Type: "string",
Doc: "Name, specify username",
Tag: `yaml:"name" required:"true" minLength:"1" maxLength:"32"`,
},
{
Name: "Age",
Type: "integer",
Doc: "Age, specify age",
Tag: `yaml:"age" required:"true" minimum:"0" maximum:"140"`,
},
{
Name: "Married",
Type: "boolean",
Doc: "Married, specify marital status [true, false]\nand optional",
Tag: `yaml:"married" required:"false"`,
},
{
Name: "Salary",
Type: "number",
Doc: "Salary, specify income status, optional",
Tag: `yaml:"salary" required:"false"`,
},
{
Name: "Children",
Type: "array of string",
Doc: "Children, specify a list of children's names, optional",
Tag: `yaml:"children" required:"false"`,
},
}
cases := []struct {
name string
model string
expected []Model
}{
{
name: "TestBasicDocTag",
model: "TestBasicDocTag",
expected: BasicStructField,
},
{
name: "TestNestedStructDocTag",
model: "TestNestedStructDocTag",
expected: []Model{
{
Name: "Struct",
Type: "array of object",
Doc: "This is the comment of the nested struct field",
Tag: `yaml:"struct" required:"true" minItems:"1" maxItems:"10"`,
Fields: BasicStructField,
},
},
},
}
p, err := NewModelParser("./testdata/doc_tag")
require.NoError(t, err)
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
m, err := p.GetModel(c.model)
require.NoError(t, err)
require.Equal(t, c.expected, m.Fields)
})
}
}

View File

@@ -0,0 +1,426 @@
// 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 types
import (
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/utils"
"github.com/fatih/structtag"
"github.com/iancoleman/orderedmap"
)
// JSONSchemaProps is a JSON-Schema following Specification Draft 4 (http://json-schema.org/).
// Borrowed from https://github.com/kubernetes/apiextensions-apiserver/blob/master/pkg/apis/apiextensions/v1/types_jsonschema.go
type JSONSchemaProps struct {
ID string `json:"id,omitempty" yaml:"id,omitempty"`
Schema JSONSchemaURL `json:"$schema,omitempty" yaml:"$schema,omitempty"`
Ref *string `json:"$ref,omitempty" yaml:"$ref,omitempty"`
Type string `json:"type,omitempty" yaml:"type,omitempty"`
Format string `json:"format,omitempty" yaml:"format,omitempty"`
Scope Scope `json:"scope,omitempty" yaml:"scope,omitempty"`
Title string `json:"title,omitempty" yaml:"title,omitempty"`
XTitleI18n map[I18nType]string `json:"x-title-i18n,omitempty" yaml:"x-title-i18n,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
XDescriptionI18n map[I18nType]string `json:"x-description-i18n,omitempty" yaml:"x-description-i18n,omitempty"`
Default *JSON `json:"default,omitempty" yaml:"default,omitempty"`
Minimum *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"`
ExclusiveMinimum bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"`
Maximum *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"`
ExclusiveMaximum bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"`
MinLength *int64 `json:"minLength,omitempty" yaml:"minLength,omitempty"`
MaxLength *int64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"`
Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"`
MaxItems *int64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"`
MinItems *int64 `json:"minItems,omitempty" yaml:"minItems,omitempty"`
UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"`
MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"`
Enum []JSON `json:"enum,omitempty" yaml:"enum,omitempty"`
MinProperties *int64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"`
MaxProperties *int64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"`
Required []string `json:"required,omitempty" yaml:"required,omitempty"`
Items *JSONSchemaPropsOrArray `json:"items,omitempty" yaml:"items,omitempty"`
AllOf []JSONSchemaProps `json:"allOf,omitempty" yaml:"allOf,omitempty"`
OneOf []JSONSchemaProps `json:"oneOf,omitempty" yaml:"oneOf,omitempty"`
AnyOf []JSONSchemaProps `json:"anyOf,omitempty" yaml:"anyOf,omitempty"`
Not *JSONSchemaProps `json:"not,omitempty" yaml:"not,omitempty"`
Properties map[string]JSONSchemaProps `json:"properties,omitempty" yaml:"properties,omitempty"`
AdditionalProperties *JSONSchemaPropsOrBool `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"`
PatternProperties map[string]JSONSchemaProps `json:"patternProperties,omitempty" yaml:"patternProperties,omitempty"`
Dependencies JSONSchemaDependencies `json:"dependencies,omitempty" yaml:"dependencies,omitempty"`
AdditionalItems *JSONSchemaPropsOrBool `json:"additionalItems,omitempty" yaml:"additionalItems,omitempty"`
Definitions JSONSchemaDefinitions `json:"definitions,omitempty" yaml:"definitions,omitempty"`
ExternalDocs *ExternalDocumentation `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"`
Example *JSON `json:"example,omitempty" yaml:"example,omitempty"`
Nullable bool `json:"nullable,omitempty" yaml:"nullable,omitempty"`
}
type Scope string
const (
ScopeGlobal Scope = "GLOBAL"
ScopeInstance Scope = "INSTANCE"
ScopeAll Scope = "ALL"
ScopeDefault = ScopeInstance
)
// JSON represents any valid JSON value.
// These types are supported: bool, int64, float64, string, []interface{}, map[string]interface{} and nil.
type JSON struct {
Raw []byte `json:"-" yaml:"-"`
}
// JSONSchemaPropsOrArray represents a value that can either be a JSONSchemaProps
// or an array of JSONSchemaProps. Mainly here for serialization purposes.
type JSONSchemaPropsOrArray struct {
Schema *JSONSchemaProps
JSONSchemas []JSONSchemaProps
}
// JSONSchemaPropsOrBool represents JSONSchemaProps or a boolean value.
// Defaults to true for the boolean property.
type JSONSchemaPropsOrBool struct {
Allows bool
Schema *JSONSchemaProps
}
// JSONSchemaDependencies represent a dependencies property.
type JSONSchemaDependencies map[string]JSONSchemaPropsOrStringArray
// JSONSchemaPropsOrStringArray represents a JSONSchemaProps or a string array.
type JSONSchemaPropsOrStringArray struct {
Schema *JSONSchemaProps
Property []string
}
// JSONSchemaURL represents a schema url.
type JSONSchemaURL string
// JSONSchemaDefinitions contains the models explicitly defined in this spec.
type JSONSchemaDefinitions map[string]JSONSchemaProps
// ExternalDocumentation allows referencing an external resource for extended documentation.
type ExternalDocumentation struct {
Description string `json:"description,omitempty" yaml:"description,omitempty"`
URL string `json:"url,omitempty" yaml:"url,omitempty"`
}
func NewJSONSchemaProps() *JSONSchemaProps {
return &JSONSchemaProps{
XTitleI18n: make(map[I18nType]string),
XDescriptionI18n: make(map[I18nType]string),
Properties: make(map[string]JSONSchemaProps),
}
}
// IsRequired determines whether the given `name` field is required
func (s *JSONSchemaProps) IsRequired(name string) bool {
req := false
for _, n := range s.Required {
if name == n {
req = true
break
}
}
return req
}
// GetDefaultValue returns the default value of the schema
func (s *JSONSchemaProps) GetDefaultValue() string {
d := "-"
if s.Default == nil {
return d
}
if len(s.Default.Raw) > 0 {
d = string(s.Default.Raw)
}
return d
}
// GetExample returns the pretty example of the schema
func (s *JSONSchemaProps) GetExample() string {
ret := ""
if s.Example != nil && len(s.Example.Raw) > 0 {
ret = string(s.Example.Raw)
if ret[0] == '{' {
// string(s.Example.Raw) might look like (when the schema is generated through go src):
// {"allow":["consumer1"],"consumers":[{"credential":"admin:123456","name":"consumer1"}]}
var obj interface{}
err := json.Unmarshal(s.Example.Raw, &obj)
if err != nil {
return ""
}
b, err := utils.MarshalYamlWithIndent(obj, 2)
if err != nil {
return ""
}
ret = string(b)
}
}
return ret
}
// GetPropertiesOrderMap converts the schema Properties map to
// an ordered map (dictionary order) and returns it
func (s *JSONSchemaProps) GetPropertiesOrderMap() *orderedmap.OrderedMap {
m := orderedmap.New()
for name, prop := range s.Properties {
m.Set(name, prop)
}
m.SortKeys(sort.Strings)
return m
}
// HandleFieldAnnotations parses the comment (annotations look like `// @<KEY> [LANGUAGE] <VALUE>`)
// and sets the schema properties
func (s *JSONSchemaProps) HandleFieldAnnotations(comment string) {
as := GetAnnotations(comment)
for _, a := range as {
switch a.Type {
case ATitle:
if s.Title == "" {
s.Title = a.Text
}
s.XTitleI18n[a.I18nType] = a.Text
case ADescription:
if s.Description == "" {
s.Description = a.Text
}
s.XDescriptionI18n[a.I18nType] = a.Text
case AScope:
s.Scope = Scope(a.Text)
case AExample:
s.Example = &JSON{Raw: []byte(a.Text)}
}
}
}
// HandleFieldTags parses the struct field tags and sets the schema properties
// TODO: Add more tags (now supported yaml, minimum, maximum, ...)
func (s *JSONSchemaProps) HandleFieldTags(tags string, parent *JSONSchemaProps, fieldName string) string {
if tags == "" {
return fieldName
}
st, err := structtag.Parse(tags)
if err != nil {
return fieldName
}
newName := fieldName
for _, tag := range st.Tags() {
switch tag.Key {
case "yaml":
newName = tag.Name
if s.Title == "" {
s.Title = newName
s.XTitleI18n[I18nDefault] = newName
}
case "required":
required, _ := strconv.ParseBool(tag.Name)
if !required {
continue
}
parent.Required = append(parent.Required, newName)
case "minimum":
min, err := strconv.ParseFloat(tag.Name, 64)
if err != nil {
continue
}
s.Minimum = &min
case "maximum":
max, err := strconv.ParseFloat(tag.Name, 64)
if err != nil {
continue
}
s.Maximum = &max
case "minLength":
minL, err := strconv.ParseInt(tag.Name, 10, 64)
if err != nil {
continue
}
s.MinLength = &minL
case "maxLength":
maxL, err := strconv.ParseInt(tag.Name, 10, 64)
if err != nil {
continue
}
s.MaxLength = &maxL
case "minItems":
minI, err := strconv.ParseInt(tag.Name, 10, 64)
if err != nil {
continue
}
s.MinItems = &minI
case "maxItems":
maxI, err := strconv.ParseInt(tag.Name, 10, 64)
if err != nil {
continue
}
s.MaxItems = &maxI
case "pattern":
s.Pattern = tag.Name
}
}
return newName
}
// JoinRequirementsBy joins the requirements by the given i18n type. Return value looks like:
// required, minLength 10, regular expression "^.*$"
func (s *JSONSchemaProps) JoinRequirementsBy(i18n I18nType, required bool) string {
reqs := s.getRequirements(required)
switch i18n {
case I18nZH_CN:
return strings.Join(reqs[I18nZH_CN], "")
case I18nEN_US:
fallthrough
default:
return strings.Join(reqs[I18nDefault], ", ")
}
}
func (s *JSONSchemaProps) getRequirements(required bool) map[I18nType][]string {
reqs := make(map[I18nType][]string)
for i18n, str := range s.GetRequired(required) {
reqs[i18n] = append(reqs[i18n], str)
}
for i18n, str := range s.GetMinimum() {
reqs[i18n] = append(reqs[i18n], str)
}
for i18n, str := range s.GetMaximum() {
reqs[i18n] = append(reqs[i18n], str)
}
for i18n, str := range s.GetMinLength() {
reqs[i18n] = append(reqs[i18n], str)
}
for i18n, str := range s.GetMaxLength() {
reqs[i18n] = append(reqs[i18n], str)
}
for i18n, str := range s.GetMinItems() {
reqs[i18n] = append(reqs[i18n], str)
}
for i18n, str := range s.GetMaxItems() {
reqs[i18n] = append(reqs[i18n], str)
}
for i18n, str := range s.GetPattern() {
reqs[i18n] = append(reqs[i18n], str)
}
return reqs
}
func (s *JSONSchemaProps) GetMinimum() map[I18nType]string {
if s.Minimum == nil {
return nil
}
return map[I18nType]string{
I18nZH_CN: fmt.Sprintf("最小值 %f", *s.Minimum),
I18nEN_US: fmt.Sprintf("minimum %f", *s.Minimum),
}
}
func (s *JSONSchemaProps) GetMaximum() map[I18nType]string {
if s.Maximum == nil {
return nil
}
return map[I18nType]string{
I18nZH_CN: fmt.Sprintf("最大值 %f", *s.Maximum),
I18nEN_US: fmt.Sprintf("maximum %f", *s.Maximum),
}
}
func (s *JSONSchemaProps) GetMinLength() map[I18nType]string {
if s.MinLength == nil {
return nil
}
return map[I18nType]string{
I18nZH_CN: fmt.Sprintf("最小长度 %d", *s.MinLength),
I18nEN_US: fmt.Sprintf("minLength %d", *s.MinLength),
}
}
func (s *JSONSchemaProps) GetMaxLength() map[I18nType]string {
if s.MaxLength == nil {
return nil
}
return map[I18nType]string{
I18nZH_CN: fmt.Sprintf("最大长度 %d", *s.MaxLength),
I18nEN_US: fmt.Sprintf("maxLength %d", *s.MaxLength),
}
}
func (s *JSONSchemaProps) GetPattern() map[I18nType]string {
if s.Pattern == "" {
return nil
}
return map[I18nType]string{
I18nZH_CN: fmt.Sprintf("正则表达式 %q", s.Pattern),
I18nEN_US: fmt.Sprintf("regular expression %q", s.Pattern),
}
}
func (s *JSONSchemaProps) GetMinItems() map[I18nType]string {
if s.MinItems == nil {
return nil
}
return map[I18nType]string{
I18nZH_CN: fmt.Sprintf("最小 item 个数 %d", *s.MinItems),
I18nEN_US: fmt.Sprintf("minItems %d", *s.MinItems),
}
}
func (s *JSONSchemaProps) GetMaxItems() map[I18nType]string {
if s.MaxItems == nil {
return nil
}
return map[I18nType]string{
I18nZH_CN: fmt.Sprintf("最大 item 个数 %d", *s.MaxItems),
I18nEN_US: fmt.Sprintf("maxItems %d", *s.MaxItems),
}
}
func (s *JSONSchemaProps) GetRequired(req bool) map[I18nType]string {
if req {
return map[I18nType]string{
I18nZH_CN: "必填",
I18nEN_US: "required",
}
}
return map[I18nType]string{
I18nZH_CN: "选填",
I18nEN_US: "optional",
}
}

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