mirror of
https://github.com/alibaba/higress.git
synced 2026-02-25 13:10:50 +08:00
Compare commits
83 Commits
v1.1.2
...
plugins/wa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d5d9c095b | ||
|
|
4bd4433248 | ||
|
|
4ea85e9a35 | ||
|
|
a140f780d2 | ||
|
|
2548815667 | ||
|
|
e760b4d0ab | ||
|
|
3cc1c7877f | ||
|
|
8039b82699 | ||
|
|
f9a015e45a | ||
|
|
5fbfbe0e4a | ||
|
|
a3339a9b1c | ||
|
|
aa94412af2 | ||
|
|
817925ef39 | ||
|
|
c55a5b9bd9 | ||
|
|
518d8dfa3d | ||
|
|
d2ee6065a0 | ||
|
|
4426f18a84 | ||
|
|
17794cef2a | ||
|
|
a554ee1ceb | ||
|
|
1dbb130539 | ||
|
|
9c1684c941 | ||
|
|
bd4109e1a4 | ||
|
|
967fa3f3d1 | ||
|
|
d57ffce1dc | ||
|
|
a2d97ae98f | ||
|
|
324e0bcf91 | ||
|
|
14742705b1 | ||
|
|
b204ad4c8d | ||
|
|
34054f8c76 | ||
|
|
6803aa44ab | ||
|
|
e5cd334d5d | ||
|
|
88c0386ca3 | ||
|
|
5174397e7c | ||
|
|
cb0479510f | ||
|
|
57b8cb1d69 | ||
|
|
9f5b795a4d | ||
|
|
26654aefc0 | ||
|
|
70176cde3e | ||
|
|
7b1f538d38 | ||
|
|
344035698a | ||
|
|
9136908354 | ||
|
|
de1dd3bfbc | ||
|
|
0c1db17de6 | ||
|
|
8cbe16f77c | ||
|
|
8ed4c5609a | ||
|
|
9ea1903ce6 | ||
|
|
6835486725 | ||
|
|
265df42456 | ||
|
|
3b1b621627 | ||
|
|
901ad9619d | ||
|
|
4a5127fedc | ||
|
|
4e44e7a1bb | ||
|
|
b54a2e7387 | ||
|
|
124caf8785 | ||
|
|
754ec71d6e | ||
|
|
d8e91851d9 | ||
|
|
86b223bc75 | ||
|
|
a18879bf86 | ||
|
|
970cfd44ee | ||
|
|
f685b0353a | ||
|
|
e135789c3e | ||
|
|
3e72d4b1f0 | ||
|
|
1ded5322a5 | ||
|
|
be8563765e | ||
|
|
45c4c80a66 | ||
|
|
d8c34bb863 | ||
|
|
4e392d1cf6 | ||
|
|
5b663ae412 | ||
|
|
fcf19535f9 | ||
|
|
14e43aa921 | ||
|
|
64ccbab29c | ||
|
|
945787f7dc | ||
|
|
792b9b0ee5 | ||
|
|
26ed9a6d93 | ||
|
|
ed36a4989f | ||
|
|
f23e26374f | ||
|
|
eb2934c084 | ||
|
|
2da1c62c69 | ||
|
|
fab734d39a | ||
|
|
2393af5c85 | ||
|
|
b142f51776 | ||
|
|
587267a733 | ||
|
|
a2078711f5 |
70
.github/workflows/build-and-test-plugin.yaml
vendored
Normal file
70
.github/workflows/build-and-test-plugin.yaml
vendored
Normal 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
|
||||
58
.github/workflows/build-and-test.yaml
vendored
58
.github/workflows/build-and-test.yaml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/build-image-and-push.yaml
vendored
2
.github/workflows/build-image-and-push.yaml
vendored
@@ -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
|
||||
|
||||
@@ -27,6 +27,7 @@ header:
|
||||
- 'tools/'
|
||||
- 'test/README.md'
|
||||
- 'pkg/cmd/hgctl/testdata/config'
|
||||
- 'pkg/cmd/hgctl/manifests'
|
||||
|
||||
comment: on-failure
|
||||
dependency:
|
||||
|
||||
@@ -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,24 +137,32 @@ 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-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-gateway-local: prebuild external/package/envoy-amd64.tar.gz external/package/envoy-arm64.tar.gz build-pilot
|
||||
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-wasmplugins:
|
||||
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-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
|
||||
|
||||
pre-install:
|
||||
@@ -168,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.1.1
|
||||
ISTIO_LATEST_IMAGE_TAG ?= 1.1.1
|
||||
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'
|
||||
@@ -249,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.
|
||||
|
||||
@@ -121,13 +121,7 @@ Higress 是基于阿里内部两年多的 Envoy Gateway 实践沉淀,以开源
|
||||
|
||||
### 联系我们
|
||||
|
||||
- Mailing list: higress@googlegroups.com
|
||||
|
||||
社区交流群:
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
开发者群:
|
||||
|
||||

|
||||
|
||||
111
envoy/1.20/patches/envoy/20231008-fallback-origin-cluster.patch
Normal file
111
envoy/1.20/patches/envoy/20231008-fallback-origin-cluster.patch
Normal 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
|
||||
};
|
||||
|
||||
315
envoy/1.20/patches/envoy/20231124-rds-optimize.patch
Normal file
315
envoy/1.20/patches/envoy/20231124-rds-optimize.patch
Normal 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
|
||||
1502
envoy/1.20/patches/envoy/20231218-dubbo-optimize.patch
Normal file
1502
envoy/1.20/patches/envoy/20231218-dubbo-optimize.patch
Normal file
File diff suppressed because one or more lines are too long
211
go.mod
211
go.mod
@@ -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,32 +33,38 @@ 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.2
|
||||
k8s.io/apimachinery v0.22.2
|
||||
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.2
|
||||
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
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.97.0 // indirect
|
||||
cloud.google.com/go v0.98.0 // indirect
|
||||
cloud.google.com/go/logging v1.4.2 // indirect
|
||||
contrib.go.opencensus.io/exporter/prometheus v0.4.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
@@ -63,51 +74,69 @@ require (
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||
github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.2 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.0 // indirect
|
||||
github.com/Microsoft/hcsshim v0.8.21 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.0 // 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/aws/aws-sdk-go v1.41.7 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // 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/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/docker/go-units v0.4.0 // 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.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/evanphx/json-patch/v5 v5.6.0 // 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 v0.4.0 // 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.14 // 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
|
||||
@@ -118,23 +147,35 @@ require (
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.1.1 // indirect
|
||||
github.com/googleapis/gnostic v0.5.5 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
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/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
|
||||
github.com/lestrrat-go/blackmagic v1.0.0 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.0 // indirect
|
||||
@@ -143,28 +184,41 @@ require (
|
||||
github.com/lestrrat-go/option v1.0.0 // indirect
|
||||
github.com/lestrrat/go-file-rotatelogs v0.0.0-20180223000712-d3151e2a480f // indirect
|
||||
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/mailru/easyjson v0.7.6 // 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/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.12 // 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
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
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
|
||||
@@ -172,23 +226,35 @@ require (
|
||||
github.com/prometheus/common v0.32.1 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/prometheus/statsd_exporter v0.21.0 // indirect
|
||||
github.com/russross/blackfriday v1.5.2 // 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.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
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
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
|
||||
@@ -197,21 +263,22 @@ require (
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v3 v3.0.1 // indirect
|
||||
gomodules.xyz/orderedmap v0.1.0 // indirect
|
||||
google.golang.org/api v0.59.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-20211020151524-b7c3a969101a // 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
|
||||
gopkg.in/ini.v1 v1.66.2 // indirect
|
||||
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/apiextensions-apiserver v0.22.2 // indirect
|
||||
k8s.io/component-base v0.22.2 // indirect
|
||||
k8s.io/klog/v2 v2.10.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20211020163157-7327e2aaee2b // 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
|
||||
sigs.k8s.io/gateway-api v0.4.0 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.8.11 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.11.0 // indirect
|
||||
@@ -230,3 +297,59 @@ replace istio.io/pkg => ./external/pkg
|
||||
replace istio.io/client-go => ./external/client-go
|
||||
|
||||
replace istio.io/istio => ./external/istio
|
||||
|
||||
require (
|
||||
github.com/evanphx/json-patch/v5 v5.6.0
|
||||
github.com/google/yamlfmt v0.10.0
|
||||
github.com/kylelemons/godebug v1.1.0
|
||||
helm.sh/helm/v3 v3.7.1
|
||||
k8s.io/apiextensions-apiserver v0.25.4
|
||||
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
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 1.1.2
|
||||
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.1.2
|
||||
version: 1.3.2
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
configSources:
|
||||
- address: "xds://127.0.0.1:15051"
|
||||
{{- if .Values.global.enableIstioAPI }}
|
||||
{{- if or .Values.global.enableIstioAPI .Values.global.enableGatewayAPI }}
|
||||
- address: "k8s://"
|
||||
{{- end }}
|
||||
|
||||
|
||||
@@ -117,3 +117,10 @@ rules:
|
||||
- apiGroups: ["config.istio.io", "security.istio.io", "networking.istio.io", "authentication.istio.io", "rbac.istio.io", "telemetry.istio.io", "extensions.istio.io"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
resources: ["*"]
|
||||
# knative KIngress configuration
|
||||
- apiGroups: ["networking.internal.knative.dev"]
|
||||
verbs: ["get","list","watch"]
|
||||
resources: ["ingresses"]
|
||||
- apiGroups: ["networking.internal.knative.dev"]
|
||||
resources: ["ingresses/status"]
|
||||
verbs: ["get","patch","update"]
|
||||
@@ -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
|
||||
@@ -126,10 +123,19 @@ spec:
|
||||
value: "{{ .Values.global.istiod.enableAnalysis }}"
|
||||
- name: CLUSTER_ID
|
||||
value: "{{ $.Values.global.multiCluster.clusterName | default `Kubernetes` }}"
|
||||
# HIGRESS_ENABLE_ISTIO_API is only used to restart the controller pod after the config change
|
||||
{{- if .Values.global.enableIstioAPI }}
|
||||
- name: HIGRESS_ENABLE_ISTIO_API
|
||||
value: "true"
|
||||
{{- end }}
|
||||
{{- if .Values.global.enableGatewayAPI }}
|
||||
- name: PILOT_ENABLE_GATEWAY_API
|
||||
value: "true"
|
||||
- name: PILOT_ENABLE_GATEWAY_API_STATUS
|
||||
value: "true"
|
||||
- name: PILOT_ENABLE_GATEWAY_API_DEPLOYMENT_CONTROLLER
|
||||
value: "false"
|
||||
{{- end }}
|
||||
{{- if not .Values.global.enableHigressIstio }}
|
||||
- name: CUSTOM_CA_CERT_NAME
|
||||
value: "higress-ca-root-cert"
|
||||
@@ -174,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
|
||||
|
||||
@@ -2,6 +2,7 @@ apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "controller.name" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "controller.labels" . | nindent 4 }}
|
||||
spec:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -17,6 +18,7 @@ global:
|
||||
local: false # When deploying to a local cluster (e.g.: kind cluster), set this to true.
|
||||
kind: false # Deprecated. Please use "global.local" instead. Will be removed later.
|
||||
enableIstioAPI: false
|
||||
enableGatewayAPI: false
|
||||
# Deprecated
|
||||
enableHigressIstio: false
|
||||
# Used to locate istiod.
|
||||
@@ -367,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: ""
|
||||
@@ -455,6 +459,8 @@ controller:
|
||||
name: "higress-controller"
|
||||
replicas: 1
|
||||
image: higress
|
||||
|
||||
hub: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress
|
||||
tag: ""
|
||||
env: {}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
dependencies:
|
||||
- name: higress-core
|
||||
repository: file://../core
|
||||
version: 1.1.2
|
||||
version: 1.3.2
|
||||
- name: higress-console
|
||||
repository: https://higress.io/helm-charts/
|
||||
version: 1.1.2
|
||||
digest: sha256:8fc099c4ad77bcdc4b9dde2ef14f89b2159b6fdcc49a3dc7e1cccb01a7ed99b9
|
||||
generated: "2023-09-19T21:46:20.2567789+08:00"
|
||||
version: 1.3.1
|
||||
digest: sha256:cf9b5f572f8e47348b3081a5620ad0165b400e4823a4ed36bd0597f3c794cbf3
|
||||
generated: "2023-12-20T19:57:57.037118+08:00"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 1.1.2
|
||||
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.1.2
|
||||
version: 1.3.2
|
||||
- name: higress-console
|
||||
repository: "https://higress.io/helm-charts/"
|
||||
version: 1.1.2
|
||||
version: 1.3.1
|
||||
type: application
|
||||
version: 1.1.2
|
||||
version: 1.3.2
|
||||
|
||||
@@ -40,6 +40,7 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| global.disableAlpnH2 | Whether to disable HTTP/2 in ALPN | true |
|
||||
| global.enableStatus | If `true`, Higress Controller will update the `status` field of Ingress resources.<br />When migrating from Nginx Ingress, in order to avoid `status` field of Ingress objects being overwritten, this parameter needs to be set to false, so Higress won't write the entry IP to the `status` field of the corresponding Ingress object. | true |
|
||||
| global.enableIstioAPI | If `true`, Higress Controller will monitor istio resources as well | false |
|
||||
| global.enableGatewayAPI | If `true`, Higress Controller will monitor Gateway API resources as well | false |
|
||||
| global.istioNamespace | The namespace istio is installed to | istio-system |
|
||||
| **Core Paramters** | | |
|
||||
| higress-core.gateway.replicas | Number of Higress Gateway pods | 2 |
|
||||
|
||||
14
istio/1.12/patches/istio/20230922-gateway-class.patch
Normal file
14
istio/1.12/patches/istio/20230922-gateway-class.patch
Normal file
@@ -0,0 +1,14 @@
|
||||
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-22 11:06:50.400535200 +0800
|
||||
+++ istio_new/pilot/pkg/config/kube/gateway/conversion.go 2023-09-22 11:07:52.954982700 +0800
|
||||
@@ -37,8 +37,8 @@
|
||||
)
|
||||
|
||||
const (
|
||||
- DefaultClassName = "istio"
|
||||
- ControllerName = "istio.io/gateway-controller"
|
||||
+ DefaultClassName = "higress"
|
||||
+ ControllerName = "higress.io/gateway-controller"
|
||||
)
|
||||
|
||||
// KubernetesResources stores all inputs to our conversion
|
||||
71
istio/1.12/patches/istio/20230925-gateway-httproute.patch
Normal file
71
istio/1.12/patches/istio/20230925-gateway-httproute.patch
Normal 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
|
||||
90
istio/1.12/patches/istio/20231008-gateway-fallback.patch
Normal file
90
istio/1.12/patches/istio/20231008-gateway-fallback.patch
Normal 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]
|
||||
13
istio/1.12/patches/istio/20231024-cds-add-fallback.patch
Normal file
13
istio/1.12/patches/istio/20231024-cds-add-fallback.patch
Normal 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)
|
||||
2202
istio/1.12/patches/istio/20231031-gatewayapi-v1beta1.patch
Normal file
2202
istio/1.12/patches/istio/20231031-gatewayapi-v1beta1.patch
Normal file
File diff suppressed because it is too large
Load Diff
377
istio/1.12/patches/istio/20231103-gatewayapi-sort.patch
Normal file
377
istio/1.12/patches/istio/20231103-gatewayapi-sort.patch
Normal 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"
|
||||
|
||||
50
istio/1.12/patches/istio/20231104-gatewayapi-catchall.patch
Normal file
50
istio/1.12/patches/istio/20231104-gatewayapi-catchall.patch
Normal 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)
|
||||
62
istio/1.12/patches/istio/20231115-optimize-xds-push.patch
Normal file
62
istio/1.12/patches/istio/20231115-optimize-xds-push.patch
Normal 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 {
|
||||
@@ -20,6 +20,10 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/common"
|
||||
"github.com/alibaba/higress/pkg/ingress/mcp"
|
||||
"github.com/alibaba/higress/pkg/ingress/translation"
|
||||
higresskube "github.com/alibaba/higress/pkg/kube"
|
||||
prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/reflection"
|
||||
@@ -46,11 +50,6 @@ import (
|
||||
"istio.io/pkg/log"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
ingressconfig "github.com/alibaba/higress/pkg/ingress/config"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/common"
|
||||
"github.com/alibaba/higress/pkg/ingress/mcp"
|
||||
higresskube "github.com/alibaba/higress/pkg/kube"
|
||||
)
|
||||
|
||||
type XdsOptions struct {
|
||||
@@ -225,9 +224,12 @@ func (s *Server) initConfigController() error {
|
||||
if options.ClusterId == "Kubernetes" {
|
||||
options.ClusterId = ""
|
||||
}
|
||||
ingressConfig := ingressconfig.NewIngressConfig(s.kubeClient, s.xdsServer, ns, options.ClusterId)
|
||||
ingressController := ingressConfig.AddLocalCluster(options)
|
||||
|
||||
ingressConfig := translation.NewIngressTranslation(s.kubeClient, s.xdsServer, ns, options.ClusterId)
|
||||
ingressController, kingressController := ingressConfig.AddLocalCluster(options)
|
||||
|
||||
s.configStores = append(s.configStores, ingressConfig)
|
||||
|
||||
// Wrap the config controller with a cache.
|
||||
aggregateConfigController, err := configaggregate.MakeCache(s.configStores)
|
||||
if err != nil {
|
||||
@@ -242,7 +244,7 @@ func (s *Server) initConfigController() error {
|
||||
|
||||
// Defer starting the controller until after the service is created.
|
||||
s.server.RunComponent(func(stop <-chan struct{}) error {
|
||||
if err := ingressConfig.InitializeCluster(ingressController, stop); err != nil {
|
||||
if err := ingressConfig.InitializeCluster(ingressController, kingressController, stop); err != nil {
|
||||
return err
|
||||
}
|
||||
go s.configController.Run(stop)
|
||||
|
||||
21
pkg/cmd/hgctl/common.go
Normal file
21
pkg/cmd/hgctl/common.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// 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
|
||||
|
||||
const (
|
||||
yamlOutput = "yaml"
|
||||
jsonOutput = "json"
|
||||
flagsOutput = "flags"
|
||||
)
|
||||
232
pkg/cmd/hgctl/completion.go
Normal file
232
pkg/cmd/hgctl/completion.go
Normal 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
|
||||
}
|
||||
@@ -33,8 +33,8 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
adminPort = 15000
|
||||
containerName = "envoy"
|
||||
defaultProxyAdminPort = 15000
|
||||
containerName = "envoy"
|
||||
)
|
||||
|
||||
func retrieveConfigDump(args []string, includeEds bool) ([]byte, error) {
|
||||
@@ -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, adminPort)
|
||||
fw, err := kubernetes.NewLocalPortForwarder(c, nn, 0, defaultProxyAdminPort, bindAddress)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -34,10 +34,11 @@ type fakePortForwarder struct {
|
||||
localPort int
|
||||
l net.Listener
|
||||
mux *http.ServeMux
|
||||
stopCh chan struct{}
|
||||
}
|
||||
|
||||
func newFakePortForwarder(b []byte) (kubernetes.PortForwarder, error) {
|
||||
p, err := kubernetes.LocalAvailablePort()
|
||||
p, err := kubernetes.LocalAvailablePort("localhost")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -46,6 +47,7 @@ func newFakePortForwarder(b []byte) (kubernetes.PortForwarder, error) {
|
||||
responseBody: b,
|
||||
localPort: p,
|
||||
mux: http.NewServeMux(),
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
fw.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write(fw.responseBody)
|
||||
@@ -54,6 +56,10 @@ func newFakePortForwarder(b []byte) (kubernetes.PortForwarder, error) {
|
||||
return fw, nil
|
||||
}
|
||||
|
||||
func (fw *fakePortForwarder) WaitForStop() {
|
||||
<-fw.stopCh
|
||||
}
|
||||
|
||||
func (fw *fakePortForwarder) Start() error {
|
||||
l, err := net.Listen("tcp", fw.Address())
|
||||
if err != nil {
|
||||
|
||||
439
pkg/cmd/hgctl/dashboard.go
Normal file
439
pkg/cmd/hgctl/dashboard.go
Normal file
@@ -0,0 +1,439 @@
|
||||
// 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 (
|
||||
"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
|
||||
controllerPort = 0
|
||||
|
||||
bindAddress = "localhost"
|
||||
|
||||
// open browser or not, default is true
|
||||
browser = true
|
||||
|
||||
// label selector
|
||||
labelSelector = ""
|
||||
|
||||
addonNamespace = ""
|
||||
|
||||
envoyDashNs = ""
|
||||
|
||||
proxyAdminPort int
|
||||
|
||||
docker = false
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPrometheusPort = 9090
|
||||
defaultGrafanaPort = 3000
|
||||
defaultConsolePort = 8080
|
||||
defaultControllerPort = 8888
|
||||
)
|
||||
|
||||
func newDashboardCmd() *cobra.Command {
|
||||
dashboardCmd := &cobra.Command{
|
||||
Use: "dashboard",
|
||||
Aliases: []string{"dash", "d"},
|
||||
Short: "Access to Higress web UIs",
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 0 {
|
||||
return fmt.Errorf("unknown dashboard %q", args[0])
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cmd.HelpFunc()(cmd, args)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
dashboardCmd.PersistentFlags().IntVarP(&listenPort, "port", "p", 0, "Local port to listen to")
|
||||
dashboardCmd.PersistentFlags().BoolVar(&browser, "browser", true,
|
||||
"When --browser is supplied as false, hgctl dashboard will not open the browser. "+
|
||||
"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.")
|
||||
dashboardCmd.AddCommand(prom)
|
||||
|
||||
graf := grafanaDashCmd()
|
||||
graf.PersistentFlags().IntVar(&grafanaPort, "ui-port", defaultGrafanaPort, "The component dashboard UI port.")
|
||||
dashboardCmd.AddCommand(graf)
|
||||
|
||||
envoy := envoyDashCmd()
|
||||
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.")
|
||||
dashboardCmd.AddCommand(envoy)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// port-forward to Higress System Prometheus; open browser
|
||||
func promDashCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "prometheus",
|
||||
Short: "Open Prometheus web UI",
|
||||
Long: `Open Higress's Prometheus dashboard`,
|
||||
Example: ` hgctl dashboard prometheus
|
||||
|
||||
# with short syntax
|
||||
hgctl dash prometheus
|
||||
hgctl d prometheus`,
|
||||
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-console-prometheus")
|
||||
if err != nil {
|
||||
return fmt.Errorf("not able to locate Prometheus pod: %v", err)
|
||||
}
|
||||
|
||||
if len(pl.Items) < 1 {
|
||||
return errors.New("no Prometheus pods found")
|
||||
}
|
||||
|
||||
// only use the first pod in the list
|
||||
return portForward(pl.Items[0].Name, addonNamespace, "Prometheus",
|
||||
"http://%s", bindAddress, promPort, client, cmd.OutOrStdout(), browser)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// port-forward to Higress System Console; open browser
|
||||
func consoleDashCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "console",
|
||||
Short: "Open Console web UI",
|
||||
Long: `Open Higress Console`,
|
||||
Example: ` hgctl dashboard console
|
||||
|
||||
# with short syntax
|
||||
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 {
|
||||
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 {
|
||||
fmt.Printf("build kubernetes CLI client fail: %v\ntry to access docker container\n", err)
|
||||
return accessDocker(cmd)
|
||||
}
|
||||
|
||||
if len(pl.Items) < 1 {
|
||||
fmt.Printf("no higress console pods found\ntry to access docker container\n")
|
||||
return accessDocker(cmd)
|
||||
}
|
||||
|
||||
// only use the first pod in the list
|
||||
return portForward(pl.Items[0].Name, addonNamespace, "Console",
|
||||
"http://%s", bindAddress, consolePort, client, cmd.OutOrStdout(), browser)
|
||||
},
|
||||
}
|
||||
|
||||
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{
|
||||
Use: "grafana",
|
||||
Short: "Open Grafana web UI",
|
||||
Long: `Open Higress's Grafana dashboard`,
|
||||
Example: ` hgctl dashboard grafana
|
||||
|
||||
# with short syntax
|
||||
hgctl dash grafana
|
||||
hgctl d grafana`,
|
||||
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-console-grafana")
|
||||
if err != nil {
|
||||
return fmt.Errorf("not able to locate Grafana pod: %v", err)
|
||||
}
|
||||
|
||||
if len(pl.Items) < 1 {
|
||||
return errors.New("no Grafana pods found")
|
||||
}
|
||||
|
||||
// only use the first pod in the list
|
||||
return portForward(pl.Items[0].Name, addonNamespace, "Grafana",
|
||||
"http://%s", bindAddress, grafanaPort, client, cmd.OutOrStdout(), browser)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// port-forward to sidecar Envoy admin port; open browser
|
||||
func envoyDashCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "envoy [<type>/]<name>[.<namespace>]",
|
||||
Short: "Open Envoy admin web UI",
|
||||
Long: `Open the Envoy admin dashboard for a higress gateway`,
|
||||
Example: ` # Open Envoy dashboard for the higress-gateway-56f9b9797-b9nnc
|
||||
hgctl dashboard envoy higress-gateway-56f9b9797-b9nnc
|
||||
|
||||
# with short syntax
|
||||
hgctl dash envoy
|
||||
hgctl d envoy
|
||||
`,
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
kubeClient, err := kubernetes.NewCLIClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
|
||||
if err != nil {
|
||||
return fmt.Errorf("build CLI client fail: %w", err)
|
||||
}
|
||||
|
||||
if labelSelector == "" && len(args) < 1 {
|
||||
c.Println(c.UsageString())
|
||||
return fmt.Errorf("specify a pod or --selector")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create k8s client: %v", err)
|
||||
}
|
||||
|
||||
var podName, ns string
|
||||
if labelSelector != "" {
|
||||
pl, err := kubeClient.PodsForSelector(envoyDashNs, labelSelector)
|
||||
if err != nil {
|
||||
return fmt.Errorf("not able to locate pod with selector %s: %v", labelSelector, err)
|
||||
}
|
||||
|
||||
if len(pl.Items) < 1 {
|
||||
return errors.New("no pods found")
|
||||
}
|
||||
// only use the first pod in the list
|
||||
podName = pl.Items[0].Name
|
||||
ns = pl.Items[0].Namespace
|
||||
} else if len(args) > 0 {
|
||||
po, err := kubeClient.Pod(types.NamespacedName{Name: args[0], Namespace: envoyDashNs})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
podName = po.Name
|
||||
ns = po.Namespace
|
||||
}
|
||||
|
||||
return portForward(podName, ns, fmt.Sprintf("Envoy sidecar %s", podName),
|
||||
"http://%s", bindAddress, proxyAdminPort, kubeClient, c.OutOrStdout(), browser)
|
||||
},
|
||||
}
|
||||
|
||||
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,
|
||||
) error {
|
||||
// port preference:
|
||||
// - If --listenPort is specified, use it
|
||||
// - without --listenPort, prefer the remotePort but fall back to a random port
|
||||
var portPrefs []int
|
||||
if listenPort != 0 {
|
||||
portPrefs = []int{listenPort}
|
||||
} else {
|
||||
portPrefs = []int{remotePort}
|
||||
}
|
||||
|
||||
var err error
|
||||
for _, localPort := range portPrefs {
|
||||
var fw kubernetes.PortForwarder
|
||||
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)
|
||||
}
|
||||
|
||||
if err := fw.Start(); err != nil {
|
||||
fw.Stop()
|
||||
// Try the next port
|
||||
continue
|
||||
}
|
||||
|
||||
// Close the port forwarder when the command is terminated.
|
||||
ClosePortForwarderOnInterrupt(fw)
|
||||
|
||||
openBrowser(fmt.Sprintf(urlFormat, fw.Address()), writer, browser)
|
||||
|
||||
// Wait for stop
|
||||
fw.WaitForStop()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failure running port forward process: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ClosePortForwarderOnInterrupt(fw kubernetes.PortForwarder) {
|
||||
go func() {
|
||||
signals := make(chan os.Signal, 1)
|
||||
signal.Notify(signals, os.Interrupt)
|
||||
defer signal.Stop(signals)
|
||||
<-signals
|
||||
fw.Stop()
|
||||
}()
|
||||
}
|
||||
|
||||
func openBrowser(url string, writer io.Writer, browser bool) {
|
||||
fmt.Fprintf(writer, "%s\n", url)
|
||||
|
||||
if !browser {
|
||||
fmt.Fprint(writer, "skipping opening a browser")
|
||||
return
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
openCommand(writer, "xdg-open", url)
|
||||
case "windows":
|
||||
openCommand(writer, "rundll32", "url.dll,FileProtocolHandler", url)
|
||||
case "darwin":
|
||||
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 {
|
||||
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())
|
||||
}
|
||||
}
|
||||
111
pkg/cmd/hgctl/docker/compose.go
Normal file
111
pkg/cmd/hgctl/docker/compose.go
Normal 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{})
|
||||
}
|
||||
365
pkg/cmd/hgctl/helm/common.go
Normal file
365
pkg/cmd/hgctl/helm/common.go
Normal file
@@ -0,0 +1,365 @@
|
||||
// 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 helm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/helm/tpath"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/util"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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 {
|
||||
return DefaultUninstallProfileName
|
||||
}
|
||||
|
||||
func ReadLayeredYAMLs(filenames []string) (string, error) {
|
||||
return readLayeredYAMLs(filenames, os.Stdin)
|
||||
}
|
||||
|
||||
func readLayeredYAMLs(filenames []string, stdinReader io.Reader) (string, error) {
|
||||
var ly string
|
||||
var stdin bool
|
||||
for _, fn := range filenames {
|
||||
var b []byte
|
||||
var err error
|
||||
if fn == "-" {
|
||||
if stdin {
|
||||
continue
|
||||
}
|
||||
stdin = true
|
||||
b, err = io.ReadAll(stdinReader)
|
||||
} else {
|
||||
b, err = os.ReadFile(strings.TrimSpace(fn))
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ly, err = util.OverlayYAML(ly, string(b))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return ly, nil
|
||||
}
|
||||
|
||||
// GetValueForSetFlag parses the passed set flags which have format key=value and if any set the given path,
|
||||
// returns the corresponding value, otherwise returns the empty string. setFlags must have valid format.
|
||||
func GetValueForSetFlag(setFlags []string, path string) string {
|
||||
ret := ""
|
||||
for _, sf := range setFlags {
|
||||
p, v := getPV(sf)
|
||||
if p == path {
|
||||
ret = v
|
||||
}
|
||||
// if set multiple times, return last set value
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// getPV returns the path and value components for the given set flag string, which must be in path=value format.
|
||||
func getPV(setFlag string) (path string, value string) {
|
||||
pv := strings.Split(setFlag, "=")
|
||||
if len(pv) != 2 {
|
||||
return setFlag, ""
|
||||
}
|
||||
path, value = strings.TrimSpace(pv[0]), strings.TrimSpace(pv[1])
|
||||
return
|
||||
}
|
||||
|
||||
func GenerateConfig(inFilenames []string, setFlags []string) (string, *Profile, string, error) {
|
||||
if err := validateSetFlags(setFlags); err != nil {
|
||||
return "", nil, "", err
|
||||
}
|
||||
|
||||
profileName, err := GetProfileFromFlags(setFlags)
|
||||
if err != nil {
|
||||
return "", nil, "", err
|
||||
}
|
||||
|
||||
valuesOverlay, err := GetValuesOverylayFromFiles(inFilenames)
|
||||
if err != nil {
|
||||
return "", nil, "", err
|
||||
}
|
||||
|
||||
profileString, profile, err := GenProfile(profileName, valuesOverlay, setFlags)
|
||||
|
||||
if err != nil {
|
||||
return "", nil, "", err
|
||||
}
|
||||
|
||||
return profileString, profile, profileName, nil
|
||||
}
|
||||
|
||||
// validateSetFlags validates that setFlags all have path=value format.
|
||||
func validateSetFlags(setFlags []string) error {
|
||||
for _, sf := range setFlags {
|
||||
pv := strings.Split(sf, "=")
|
||||
if len(pv) != 2 {
|
||||
return fmt.Errorf("set flag %s has incorrect format, must be path=value", sf)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func overlaySetFlagValues(iopYAML string, setFlags []string) (string, error) {
|
||||
iop := make(map[string]any)
|
||||
if err := yaml.Unmarshal([]byte(iopYAML), &iop); err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Unmarshal returns nil for empty manifests but we need something to insert into.
|
||||
if iop == nil {
|
||||
iop = make(map[string]any)
|
||||
}
|
||||
|
||||
for _, sf := range setFlags {
|
||||
p, v := getPV(sf)
|
||||
inc, _, err := tpath.GetPathContext(iop, util.PathFromString(p), true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// input value type is always string, transform it to correct type before setting.
|
||||
if err := tpath.WritePathContext(inc, util.ParseValue(v), false); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
out, err := yaml.Marshal(iop)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
// getInstallPackagePath returns the installPackagePath in the given IstioOperator YAML string.
|
||||
func getInstallPackagePath(profileYAML string) (string, error) {
|
||||
profile, err := UnmarshalProfile(profileYAML)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if profile == nil {
|
||||
return "", nil
|
||||
}
|
||||
return profile.InstallPackagePath, nil
|
||||
}
|
||||
|
||||
// GetProfileYAML returns the YAML for the given profile name, using the given profileOrPath string, which may be either
|
||||
// a profile label or a file path.
|
||||
func GetProfileYAML(installPackagePath, profileOrPath string) (string, error) {
|
||||
if profileOrPath == "" {
|
||||
profileOrPath = DefaultProfileFilename
|
||||
}
|
||||
profiles, err := readProfiles(installPackagePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read profiles: %v", err)
|
||||
}
|
||||
// If charts are a file path and profile is a name like default, transform it to the file path.
|
||||
if profiles[profileOrPath] && installPackagePath != "" {
|
||||
profileOrPath = filepath.Join(installPackagePath, "profiles", profileOrPath+".yaml")
|
||||
}
|
||||
// This contains the IstioOperator CR.
|
||||
baseCRYAML, err := ReadProfileYAML(profileOrPath, installPackagePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
//if !IsDefaultProfile(profileOrPath) {
|
||||
// // Profile definitions are relative to the default profileOrPath, so read that first.
|
||||
// dfn := DefaultFilenameForProfile(profileOrPath)
|
||||
// defaultYAML, err := ReadProfileYAML(dfn, installPackagePath)
|
||||
// if err != nil {
|
||||
// return "", err
|
||||
// }
|
||||
// baseCRYAML, err = util.OverlayYAML(defaultYAML, baseCRYAML)
|
||||
// if err != nil {
|
||||
// return "", err
|
||||
// }
|
||||
//}
|
||||
return baseCRYAML, nil
|
||||
}
|
||||
|
||||
// IsDefaultProfile reports whether the given profile is the default profile.
|
||||
func IsDefaultProfile(profile string) bool {
|
||||
return profile == "" || profile == DefaultProfileName || filepath.Base(profile) == DefaultProfileFilename
|
||||
}
|
||||
|
||||
// DefaultFilenameForProfile returns the profile name of the default profile for the given profile.
|
||||
func DefaultFilenameForProfile(profile string) string {
|
||||
switch {
|
||||
case util.IsFilePath(profile):
|
||||
return filepath.Join(filepath.Dir(profile), DefaultProfileFilename)
|
||||
default:
|
||||
return DefaultProfileName
|
||||
}
|
||||
}
|
||||
|
||||
// ReadProfileYAML reads the YAML values associated with the given profile. It uses an appropriate reader for the
|
||||
// profile format (compiled-in, file, HTTP, etc.).
|
||||
func ReadProfileYAML(profile, manifestsPath string) (string, error) {
|
||||
var err error
|
||||
var globalValues string
|
||||
|
||||
// Get global values from profile.
|
||||
switch {
|
||||
case util.IsFilePath(profile):
|
||||
if globalValues, err = readFile(profile); err != nil {
|
||||
return "", err
|
||||
}
|
||||
default:
|
||||
if globalValues, err = LoadValues(profile, manifestsPath); err != nil {
|
||||
return "", fmt.Errorf("failed to read profile %v from %v: %v", profile, manifestsPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return globalValues, nil
|
||||
}
|
||||
|
||||
func readFile(path string) (string, error) {
|
||||
b, err := os.ReadFile(path)
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
// UnmarshalProfile unmarshals a string containing Profile as YAML.
|
||||
func UnmarshalProfile(profileYAML string) (*Profile, error) {
|
||||
profile := &Profile{}
|
||||
if err := yaml.Unmarshal([]byte(profileYAML), profile); err != nil {
|
||||
return nil, fmt.Errorf("%s:\n\nYAML:\n%s", err, profileYAML)
|
||||
}
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
// GenProfile generates an Profile from the given profile name or path, and overlay YAMLs from user
|
||||
// files and the --set flag. If successful, it returns an Profile string and struct.
|
||||
func GenProfile(profileOrPath, 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
|
||||
}
|
||||
|
||||
// To generate the base profileOrPath for overlaying with user values, we need the installPackagePath where the profiles
|
||||
// can be found, and the selected profileOrPath. Both of these can come from either the user overlay file or --set flag.
|
||||
outYAML, err := GetProfileYAML(installPackagePath, profileOrPath)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// 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(outYAML, 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
|
||||
}
|
||||
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
|
||||
}
|
||||
return util.ToYAML(finalProfile), finalProfile, nil
|
||||
}
|
||||
63
pkg/cmd/hgctl/helm/name/name.go
Normal file
63
pkg/cmd/hgctl/helm/name/name.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// 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 name
|
||||
|
||||
// Kubernetes Kind strings.
|
||||
const (
|
||||
CRDStr = "CustomResourceDefinition"
|
||||
ClusterRoleStr = "ClusterRole"
|
||||
ClusterRoleBindingStr = "ClusterRoleBinding"
|
||||
CMStr = "ConfigMap"
|
||||
DaemonSetStr = "DaemonSet"
|
||||
DeploymentStr = "Deployment"
|
||||
EndpointStr = "Endpoints"
|
||||
HPAStr = "HorizontalPodAutoscaler"
|
||||
IngressStr = "Ingress"
|
||||
IstioOperator = "IstioOperator"
|
||||
MutatingWebhookConfigurationStr = "MutatingWebhookConfiguration"
|
||||
NamespaceStr = "Namespace"
|
||||
PVCStr = "PersistentVolumeClaim"
|
||||
PodStr = "Pod"
|
||||
PDBStr = "PodDisruptionBudget"
|
||||
ReplicationControllerStr = "ReplicationController"
|
||||
ReplicaSetStr = "ReplicaSet"
|
||||
RoleStr = "Role"
|
||||
RoleBindingStr = "RoleBinding"
|
||||
SAStr = "ServiceAccount"
|
||||
ServiceStr = "Service"
|
||||
SecretStr = "Secret"
|
||||
StatefulSetStr = "StatefulSet"
|
||||
ValidatingWebhookConfigurationStr = "ValidatingWebhookConfiguration"
|
||||
)
|
||||
|
||||
// Istio Kind strings
|
||||
const (
|
||||
EnvoyFilterStr = "EnvoyFilter"
|
||||
GatewayStr = "Gateway"
|
||||
DestinationRuleStr = "DestinationRule"
|
||||
MeshPolicyStr = "MeshPolicy"
|
||||
PeerAuthenticationStr = "PeerAuthentication"
|
||||
VirtualServiceStr = "VirtualService"
|
||||
IstioOperatorStr = "IstioOperator"
|
||||
)
|
||||
|
||||
// Istio API Group Names
|
||||
const (
|
||||
AuthenticationAPIGroupName = "authentication.istio.io"
|
||||
ConfigAPIGroupName = "config.istio.io"
|
||||
NetworkingAPIGroupName = "networking.istio.io"
|
||||
OperatorAPIGroupName = "operator.istio.io"
|
||||
SecurityAPIGroupName = "security.istio.io"
|
||||
)
|
||||
573
pkg/cmd/hgctl/helm/object/objects.go
Normal file
573
pkg/cmd/hgctl/helm/object/objects.go
Normal file
@@ -0,0 +1,573 @@
|
||||
// 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 object
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
names "github.com/alibaba/higress/pkg/cmd/hgctl/helm/name"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/helm/tpath"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/util"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
const (
|
||||
// YAMLSeparator is a separator for multi-document YAML files.
|
||||
YAMLSeparator = "\n---\n"
|
||||
)
|
||||
|
||||
// K8sObject is an in-memory representation of a k8s object, used for moving between different representations
|
||||
// (Unstructured, JSON, YAML) with cached rendering.
|
||||
type K8sObject struct {
|
||||
object *unstructured.Unstructured
|
||||
|
||||
Group string
|
||||
Kind string
|
||||
Name string
|
||||
Namespace string
|
||||
|
||||
json []byte
|
||||
yaml []byte
|
||||
}
|
||||
|
||||
// NewK8sObject creates a new K8sObject and returns a ptr to it.
|
||||
func NewK8sObject(u *unstructured.Unstructured, json, yaml []byte) *K8sObject {
|
||||
o := &K8sObject{
|
||||
object: u,
|
||||
json: json,
|
||||
yaml: yaml,
|
||||
}
|
||||
|
||||
gvk := u.GetObjectKind().GroupVersionKind()
|
||||
o.Group = gvk.Group
|
||||
o.Kind = gvk.Kind
|
||||
o.Name = u.GetName()
|
||||
o.Namespace = u.GetNamespace()
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
// Hash returns a unique, insecure hash based on kind, namespace and name.
|
||||
func Hash(kind, namespace, name string) string {
|
||||
switch kind {
|
||||
case names.ClusterRoleStr, names.ClusterRoleBindingStr, names.MeshPolicyStr:
|
||||
namespace = ""
|
||||
}
|
||||
return strings.Join([]string{kind, namespace, name}, ":")
|
||||
}
|
||||
|
||||
// FromHash parses kind, namespace and name from a hash.
|
||||
func FromHash(hash string) (kind, namespace, name string) {
|
||||
hv := strings.Split(hash, ":")
|
||||
if len(hv) != 3 {
|
||||
return "Bad hash string: " + hash, "", ""
|
||||
}
|
||||
kind, namespace, name = hv[0], hv[1], hv[2]
|
||||
return
|
||||
}
|
||||
|
||||
// HashNameKind returns a unique, insecure hash based on kind and name.
|
||||
func HashNameKind(kind, name string) string {
|
||||
return strings.Join([]string{kind, name}, ":")
|
||||
}
|
||||
|
||||
// ParseJSONToK8sObject parses JSON to an K8sObject.
|
||||
func ParseJSONToK8sObject(json []byte) (*K8sObject, error) {
|
||||
o, _, err := unstructured.UnstructuredJSONScheme.Decode(json, nil, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing json into unstructured object: %v", err)
|
||||
}
|
||||
|
||||
u, ok := o.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("parsed unexpected type %T", o)
|
||||
}
|
||||
|
||||
return NewK8sObject(u, json, nil), nil
|
||||
}
|
||||
|
||||
// ParseYAMLToK8sObject parses YAML to an Object.
|
||||
func ParseYAMLToK8sObject(yaml []byte) (*K8sObject, error) {
|
||||
r := bytes.NewReader(yaml)
|
||||
decoder := k8syaml.NewYAMLOrJSONDecoder(r, 1024)
|
||||
|
||||
out := &unstructured.Unstructured{}
|
||||
err := decoder.Decode(out)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding object %v: %v", string(yaml), err)
|
||||
}
|
||||
return NewK8sObject(out, nil, yaml), nil
|
||||
}
|
||||
|
||||
// UnstructuredObject exposes the raw object, primarily for testing
|
||||
func (o *K8sObject) UnstructuredObject() *unstructured.Unstructured {
|
||||
return o.object
|
||||
}
|
||||
|
||||
// ResolveK8sConflict - This method resolves k8s object possible
|
||||
// conflicting settings. Which K8sObjects may need such method
|
||||
// depends on the type of the K8sObject.
|
||||
func (o *K8sObject) ResolveK8sConflict() *K8sObject {
|
||||
if o.Kind == names.PDBStr {
|
||||
return resolvePDBConflict(o)
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
// Unstructured exposes the raw object content, primarily for testing
|
||||
func (o *K8sObject) Unstructured() map[string]any {
|
||||
return o.UnstructuredObject().UnstructuredContent()
|
||||
}
|
||||
|
||||
// Container returns a container subtree for Deployment objects if one is found, or nil otherwise.
|
||||
func (o *K8sObject) Container(name string) map[string]any {
|
||||
u := o.Unstructured()
|
||||
path := fmt.Sprintf("spec.template.spec.containers.[name:%s]", name)
|
||||
node, f, err := tpath.GetPathContext(u, util.PathFromString(path), false)
|
||||
if err == nil && f {
|
||||
// Must be the type from the schema.
|
||||
return node.Node.(map[string]any)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GroupVersionKind returns the GroupVersionKind for the K8sObject
|
||||
func (o *K8sObject) GroupVersionKind() schema.GroupVersionKind {
|
||||
return o.object.GroupVersionKind()
|
||||
}
|
||||
|
||||
// Version returns the APIVersion of the K8sObject
|
||||
func (o *K8sObject) Version() string {
|
||||
return o.object.GetAPIVersion()
|
||||
}
|
||||
|
||||
// Hash returns a unique hash for the K8sObject
|
||||
func (o *K8sObject) Hash() string {
|
||||
return Hash(o.Kind, o.Namespace, o.Name)
|
||||
}
|
||||
|
||||
// HashNameKind returns a hash for the K8sObject based on the name and kind only.
|
||||
func (o *K8sObject) HashNameKind() string {
|
||||
return HashNameKind(o.Kind, o.Name)
|
||||
}
|
||||
|
||||
// JSON returns a JSON representation of the K8sObject, using an internal cache.
|
||||
func (o *K8sObject) JSON() ([]byte, error) {
|
||||
if o.json != nil {
|
||||
return o.json, nil
|
||||
}
|
||||
|
||||
b, err := o.object.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// YAML returns a YAML representation of the K8sObject, using an internal cache.
|
||||
func (o *K8sObject) YAML() ([]byte, error) {
|
||||
if o == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if o.yaml != nil {
|
||||
return o.yaml, nil
|
||||
}
|
||||
oj, err := o.JSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o.json = oj
|
||||
y, err := yaml.JSONToYAML(oj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o.yaml = y
|
||||
return y, nil
|
||||
}
|
||||
|
||||
// YAMLDebugString returns a YAML representation of the K8sObject, or an error string if the K8sObject cannot be rendered to YAML.
|
||||
func (o *K8sObject) YAMLDebugString() string {
|
||||
y, err := o.YAML()
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return string(y)
|
||||
}
|
||||
|
||||
// K8sObjects holds a collection of k8s objects, so that we can filter / sequence them
|
||||
type K8sObjects []*K8sObject
|
||||
|
||||
// String implements the Stringer interface.
|
||||
func (os K8sObjects) String() string {
|
||||
var out []string
|
||||
for _, oo := range os {
|
||||
out = append(out, oo.YAMLDebugString())
|
||||
}
|
||||
return strings.Join(out, YAMLSeparator)
|
||||
}
|
||||
|
||||
// Keys returns a slice with the keys of os.
|
||||
func (os K8sObjects) Keys() []string {
|
||||
var out []string
|
||||
for _, oo := range os {
|
||||
out = append(out, oo.Hash())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// UnstructuredItems returns the list of items of unstructured.Unstructured.
|
||||
func (os K8sObjects) UnstructuredItems() []unstructured.Unstructured {
|
||||
var usList []unstructured.Unstructured
|
||||
for _, obj := range os {
|
||||
usList = append(usList, *obj.UnstructuredObject())
|
||||
}
|
||||
return usList
|
||||
}
|
||||
|
||||
// ParseK8sObjectsFromYAMLManifest returns a K8sObjects representation of manifest.
|
||||
func ParseK8sObjectsFromYAMLManifest(manifest string) (K8sObjects, error) {
|
||||
return ParseK8sObjectsFromYAMLManifestFailOption(manifest, true)
|
||||
}
|
||||
|
||||
// ParseK8sObjectsFromYAMLManifestFailOption returns a K8sObjects representation of manifest. Continues parsing when a bad object
|
||||
// is found if failOnError is set to false.
|
||||
func ParseK8sObjectsFromYAMLManifestFailOption(manifest string, failOnError bool) (K8sObjects, error) {
|
||||
var b bytes.Buffer
|
||||
|
||||
var yamls []string
|
||||
scanner := bufio.NewScanner(strings.NewReader(manifest))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "---") {
|
||||
// yaml separator
|
||||
yamls = append(yamls, b.String())
|
||||
b.Reset()
|
||||
} else {
|
||||
if _, err := b.WriteString(line); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := b.WriteString("\n"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
yamls = append(yamls, b.String())
|
||||
|
||||
var objects K8sObjects
|
||||
|
||||
for _, yaml := range yamls {
|
||||
yaml = removeNonYAMLLines(yaml)
|
||||
if yaml == "" {
|
||||
continue
|
||||
}
|
||||
o, err := ParseYAMLToK8sObject([]byte(yaml))
|
||||
if err != nil {
|
||||
e := fmt.Errorf("failed to parse YAML to a k8s object: %s", err)
|
||||
if failOnError {
|
||||
return nil, e
|
||||
}
|
||||
continue
|
||||
}
|
||||
if o.Valid() {
|
||||
objects = append(objects, o)
|
||||
}
|
||||
}
|
||||
|
||||
return objects, nil
|
||||
}
|
||||
|
||||
func removeNonYAMLLines(yms string) string {
|
||||
var b strings.Builder
|
||||
for _, s := range strings.Split(yms, "\n") {
|
||||
if strings.HasPrefix(s, "#") {
|
||||
continue
|
||||
}
|
||||
b.WriteString(s)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
// helm charts sometimes emits blank objects with just a "disabled" comment.
|
||||
return strings.TrimSpace(b.String())
|
||||
}
|
||||
|
||||
// YAMLManifest returns a YAML representation of K8sObjects os.
|
||||
func (os K8sObjects) YAMLManifest() (string, error) {
|
||||
var b bytes.Buffer
|
||||
|
||||
for i, item := range os {
|
||||
if i != 0 {
|
||||
if _, err := b.WriteString("\n\n"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
ym, err := item.YAML()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error building yaml: %v", err)
|
||||
}
|
||||
if _, err := b.Write(ym); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := b.Write([]byte(YAMLSeparator)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
// Sort will order the items in K8sObjects in order of score, group, kind, name. The intent is to
|
||||
// have a deterministic ordering in which K8sObjects are applied.
|
||||
func (os K8sObjects) Sort(score func(o *K8sObject) int) {
|
||||
sort.Slice(os, func(i, j int) bool {
|
||||
iScore := score(os[i])
|
||||
jScore := score(os[j])
|
||||
return iScore < jScore ||
|
||||
(iScore == jScore &&
|
||||
os[i].Group < os[j].Group) ||
|
||||
(iScore == jScore &&
|
||||
os[i].Group == os[j].Group &&
|
||||
os[i].Kind < os[j].Kind) ||
|
||||
(iScore == jScore &&
|
||||
os[i].Group == os[j].Group &&
|
||||
os[i].Kind == os[j].Kind &&
|
||||
os[i].Name < os[j].Name)
|
||||
})
|
||||
}
|
||||
|
||||
// ToMap returns a map of K8sObject hash to K8sObject.
|
||||
func (os K8sObjects) ToMap() map[string]*K8sObject {
|
||||
ret := make(map[string]*K8sObject)
|
||||
for _, oo := range os {
|
||||
if oo.Valid() {
|
||||
ret[oo.Hash()] = oo
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// ToNameKindMap returns a map of K8sObject name/kind hash to K8sObject.
|
||||
func (os K8sObjects) ToNameKindMap() map[string]*K8sObject {
|
||||
ret := make(map[string]*K8sObject)
|
||||
for _, oo := range os {
|
||||
if oo.Valid() {
|
||||
ret[oo.HashNameKind()] = oo
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Valid checks returns true if Kind of K8sObject is not empty.
|
||||
func (o *K8sObject) Valid() bool {
|
||||
return o.Kind != ""
|
||||
}
|
||||
|
||||
// FullName returns namespace/name of K8s object
|
||||
func (o *K8sObject) FullName() string {
|
||||
return fmt.Sprintf("%s/%s", o.Namespace, o.Name)
|
||||
}
|
||||
|
||||
// Equal returns true if o and other are both valid and equal to each other.
|
||||
func (o *K8sObject) Equal(other *K8sObject) bool {
|
||||
if o == nil {
|
||||
return other == nil
|
||||
}
|
||||
if other == nil {
|
||||
return o == nil
|
||||
}
|
||||
|
||||
ay, err := o.YAML()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
by, err := other.YAML()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return util.IsYAMLEqual(string(ay), string(by))
|
||||
}
|
||||
|
||||
func istioCustomResources(group string) bool {
|
||||
switch group {
|
||||
case names.ConfigAPIGroupName,
|
||||
names.SecurityAPIGroupName,
|
||||
names.AuthenticationAPIGroupName,
|
||||
names.NetworkingAPIGroupName:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DefaultObjectOrder is default sorting function used to sort k8s objects.
|
||||
func DefaultObjectOrder() func(o *K8sObject) int {
|
||||
return func(o *K8sObject) int {
|
||||
gk := o.Group + "/" + o.Kind
|
||||
switch {
|
||||
// Create CRDs asap - both because they are slow and because we will likely create instances of them soon
|
||||
case gk == "apiextensions.k8s.io/CustomResourceDefinition":
|
||||
return -1000
|
||||
|
||||
// We need to create ServiceAccounts, Roles before we bind them with a RoleBinding
|
||||
case gk == "/ServiceAccount" || gk == "rbac.authorization.k8s.io/ClusterRole":
|
||||
return 1
|
||||
case gk == "rbac.authorization.k8s.io/ClusterRoleBinding":
|
||||
return 2
|
||||
|
||||
// validatingwebhookconfiguration is configured to FAIL-OPEN in the default install. For the
|
||||
// re-install case we want to apply the validatingwebhookconfiguration first to reset any
|
||||
// orphaned validatingwebhookconfiguration that is FAIL-CLOSE.
|
||||
case gk == "admissionregistration.k8s.io/ValidatingWebhookConfiguration":
|
||||
return 3
|
||||
|
||||
case istioCustomResources(o.Group):
|
||||
return 4
|
||||
|
||||
// Pods might need configmap or secrets - avoid backoff by creating them first
|
||||
case gk == "/ConfigMap" || gk == "/Secrets":
|
||||
return 100
|
||||
|
||||
// Create the pods after we've created other things they might be waiting for
|
||||
case gk == "extensions/Deployment" || gk == "app/Deployment":
|
||||
return 1000
|
||||
|
||||
// Autoscalers typically act on a deployment
|
||||
case gk == "autoscaling/HorizontalPodAutoscaler":
|
||||
return 1001
|
||||
|
||||
// Create services late - after pods have been started
|
||||
case gk == "/Service":
|
||||
return 10000
|
||||
|
||||
default:
|
||||
return 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ObjectsNotInLists(objects K8sObjects, lists ...K8sObjects) K8sObjects {
|
||||
var ret K8sObjects
|
||||
|
||||
filterMap := make(map[*K8sObject]bool)
|
||||
for _, list := range lists {
|
||||
for _, object := range list {
|
||||
filterMap[object] = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, o := range objects {
|
||||
if !filterMap[o] {
|
||||
ret = append(ret, o)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// KindObjects returns the subset of objs with the given kind.
|
||||
func KindObjects(objs K8sObjects, kind string) K8sObjects {
|
||||
var ret K8sObjects
|
||||
for _, o := range objs {
|
||||
if o.Kind == kind {
|
||||
ret = append(ret, o)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
//// ParseK8SYAMLToIstioOperator parses a IstioOperator CustomResource YAML string and unmarshals in into
|
||||
//// an IstioOperatorSpec object. It returns the object and an API group/version with it.
|
||||
//func ParseK8SYAMLToIstioOperator(yml string) (*v1alpha1.HigressOperator, *schema.GroupVersionKind, error) {
|
||||
// o, err := ParseYAMLToK8sObject([]byte(yml))
|
||||
// if err != nil {
|
||||
// return nil, nil, err
|
||||
// }
|
||||
// iop := &v1alpha1.HigressOperator{}
|
||||
// if err := yaml.UnmarshalStrict([]byte(yml), iop); err != nil {
|
||||
// return nil, nil, err
|
||||
// }
|
||||
// gvk := o.GroupVersionKind()
|
||||
// //v1alpha1.SetNamespace(iop.Spec, o.Namespace)
|
||||
// return iop, &gvk, nil
|
||||
//}
|
||||
|
||||
// AllObjectHashes returns a map with object hashes of all the objects contained in cmm as the keys.
|
||||
func AllObjectHashes(m string) map[string]bool {
|
||||
ret := make(map[string]bool)
|
||||
objs, err := ParseK8sObjectsFromYAMLManifest(m)
|
||||
if err != nil {
|
||||
}
|
||||
for _, o := range objs {
|
||||
ret[o.Hash()] = true
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// resolvePDBConflict When user uses both minAvailable and
|
||||
// maxUnavailable to configure istio instances, these two
|
||||
// parameters are mutually exclusive, care must be taken
|
||||
// to resolve the issue
|
||||
func resolvePDBConflict(o *K8sObject) *K8sObject {
|
||||
if o.json == nil {
|
||||
return o
|
||||
}
|
||||
if o.object.Object["spec"] == nil {
|
||||
return o
|
||||
}
|
||||
spec := o.object.Object["spec"].(map[string]any)
|
||||
isDefault := func(item any) bool {
|
||||
var ii intstr.IntOrString
|
||||
switch item := item.(type) {
|
||||
case int:
|
||||
ii = intstr.FromInt(item)
|
||||
case int64:
|
||||
ii = intstr.FromInt(int(item))
|
||||
case string:
|
||||
ii = intstr.FromString(item)
|
||||
default:
|
||||
ii = intstr.FromInt(0)
|
||||
}
|
||||
intVal, err := intstr.GetScaledValueFromIntOrPercent(&ii, 100, false)
|
||||
if err != nil || intVal == 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
if spec["maxUnavailable"] != nil && spec["minAvailable"] != nil {
|
||||
// When both maxUnavailable and minAvailable present and
|
||||
// neither has value 0, this is considered a conflict,
|
||||
// then maxUnavailale will take precedence.
|
||||
if !isDefault(spec["maxUnavailable"]) && !isDefault(spec["minAvailable"]) {
|
||||
delete(spec, "minAvailable")
|
||||
// Make sure that the json and yaml representation of the object
|
||||
// is consistent with the changed object
|
||||
o.json = nil
|
||||
o.json, _ = o.JSON()
|
||||
if o.yaml != nil {
|
||||
o.yaml = nil
|
||||
o.yaml, _ = o.YAML()
|
||||
}
|
||||
}
|
||||
}
|
||||
return o
|
||||
}
|
||||
713
pkg/cmd/hgctl/helm/object/objects_test.go
Normal file
713
pkg/cmd/hgctl/helm/object/objects_test.go
Normal file
@@ -0,0 +1,713 @@
|
||||
// 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 object
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/util"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
func TestHash(t *testing.T) {
|
||||
hashTests := []struct {
|
||||
desc string
|
||||
kind string
|
||||
namespace string
|
||||
name string
|
||||
want string
|
||||
}{
|
||||
{"CalculateHashForObjectWithNormalCharacter", "Service", "default", "ingressgateway", "Service:default:ingressgateway"},
|
||||
{"CalculateHashForObjectWithDash", "Deployment", "istio-system", "istio-pilot", "Deployment:istio-system:istio-pilot"},
|
||||
{"CalculateHashForObjectWithDot", "ConfigMap", "istio-system", "my.config", "ConfigMap:istio-system:my.config"},
|
||||
}
|
||||
|
||||
for _, tt := range hashTests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
got := Hash(tt.kind, tt.namespace, tt.name)
|
||||
if got != tt.want {
|
||||
t.Errorf("Hash(%s): got %s for kind %s, namespace %s, name %s, want %s", tt.desc, got, tt.kind, tt.namespace, tt.name, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromHash(t *testing.T) {
|
||||
hashTests := []struct {
|
||||
desc string
|
||||
hash string
|
||||
kind string
|
||||
namespace string
|
||||
name string
|
||||
}{
|
||||
{"ParseHashWithNormalCharacter", "Service:default:ingressgateway", "Service", "default", "ingressgateway"},
|
||||
{"ParseHashForObjectWithDash", "Deployment:istio-system:istio-pilot", "Deployment", "istio-system", "istio-pilot"},
|
||||
{"ParseHashForObjectWithDot", "ConfigMap:istio-system:my.config", "ConfigMap", "istio-system", "my.config"},
|
||||
{"InvalidHash", "test", "Bad hash string: test", "", ""},
|
||||
}
|
||||
|
||||
for _, tt := range hashTests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
k, ns, name := FromHash(tt.hash)
|
||||
if k != tt.kind || ns != tt.namespace || name != tt.name {
|
||||
t.Errorf("FromHash(%s): got kind %s, namespace %s, name %s, want kind %s, namespace %s, name %s", tt.desc, k, ns, name, tt.kind, tt.namespace, tt.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashNameKind(t *testing.T) {
|
||||
hashNameKindTests := []struct {
|
||||
desc string
|
||||
kind string
|
||||
name string
|
||||
want string
|
||||
}{
|
||||
{"CalculateHashNameKindForObjectWithNormalCharacter", "Service", "ingressgateway", "Service:ingressgateway"},
|
||||
{"CalculateHashNameKindForObjectWithDash", "Deployment", "istio-pilot", "Deployment:istio-pilot"},
|
||||
{"CalculateHashNameKindForObjectWithDot", "ConfigMap", "my.config", "ConfigMap:my.config"},
|
||||
}
|
||||
|
||||
for _, tt := range hashNameKindTests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
got := HashNameKind(tt.kind, tt.name)
|
||||
if got != tt.want {
|
||||
t.Errorf("HashNameKind(%s): got %s for kind %s, name %s, want %s", tt.desc, got, tt.kind, tt.name, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseJSONToK8sObject(t *testing.T) {
|
||||
testDeploymentJSON := `{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": {
|
||||
"name": "istio-citadel",
|
||||
"namespace": "istio-system",
|
||||
"labels": {
|
||||
"istio": "citadel"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"replicas": 1,
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"istio": "citadel"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"istio": "citadel"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"name": "citadel",
|
||||
"image": "docker.io/istio/citadel:1.1.8",
|
||||
"args": [
|
||||
"--append-dns-names=true",
|
||||
"--grpc-port=8060",
|
||||
"--grpc-hostname=citadel",
|
||||
"--citadel-storage-namespace=istio-system",
|
||||
"--custom-dns-names=istio-pilot-service-account.istio-system:istio-pilot.istio-system",
|
||||
"--monitoring-port=15014",
|
||||
"--self-signed-ca=true"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
testPodJSON := `{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"name": "istio-galley-75bcd59768-hpt5t",
|
||||
"namespace": "istio-system",
|
||||
"labels": {
|
||||
"istio": "galley"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"name": "galley",
|
||||
"image": "docker.io/istio/galley:1.1.8",
|
||||
"command": [
|
||||
"/usr/local/bin/galley",
|
||||
"server",
|
||||
"--meshConfigFile=/etc/mesh-config/mesh",
|
||||
"--livenessProbeInterval=1s",
|
||||
"--livenessProbePath=/healthliveness",
|
||||
"--readinessProbePath=/healthready",
|
||||
"--readinessProbeInterval=1s",
|
||||
"--deployment-namespace=istio-system",
|
||||
"--insecure=true",
|
||||
"--validation-webhook-config-file",
|
||||
"/etc/config/validatingwebhookconfiguration.yaml",
|
||||
"--monitoringPort=15014",
|
||||
"--log_output_level=default:info"
|
||||
],
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 443,
|
||||
"protocol": "TCP"
|
||||
},
|
||||
{
|
||||
"containerPort": 15014,
|
||||
"protocol": "TCP"
|
||||
},
|
||||
{
|
||||
"containerPort": 9901,
|
||||
"protocol": "TCP"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}`
|
||||
testServiceJSON := `{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Service",
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"app": "pilot"
|
||||
},
|
||||
"name": "istio-pilot",
|
||||
"namespace": "istio-system"
|
||||
},
|
||||
"spec": {
|
||||
"clusterIP": "10.102.230.31",
|
||||
"ports": [
|
||||
{
|
||||
"name": "grpc-xds",
|
||||
"port": 15010,
|
||||
"protocol": "TCP",
|
||||
"targetPort": 15010
|
||||
},
|
||||
{
|
||||
"name": "https-xds",
|
||||
"port": 15011,
|
||||
"protocol": "TCP",
|
||||
"targetPort": 15011
|
||||
},
|
||||
{
|
||||
"name": "http-legacy-discovery",
|
||||
"port": 8080,
|
||||
"protocol": "TCP",
|
||||
"targetPort": 8080
|
||||
},
|
||||
{
|
||||
"name": "http-monitoring",
|
||||
"port": 15014,
|
||||
"protocol": "TCP",
|
||||
"targetPort": 15014
|
||||
}
|
||||
],
|
||||
"selector": {
|
||||
"istio": "pilot"
|
||||
},
|
||||
"sessionAffinity": "None",
|
||||
"type": "ClusterIP"
|
||||
}
|
||||
}`
|
||||
|
||||
testInvalidJSON := `invalid json`
|
||||
|
||||
parseJSONToK8sObjectTests := []struct {
|
||||
desc string
|
||||
objString string
|
||||
wantGroup string
|
||||
wantKind string
|
||||
wantName string
|
||||
wantNamespace string
|
||||
wantErr bool
|
||||
}{
|
||||
{"ParseJsonToK8sDeployment", testDeploymentJSON, "apps", "Deployment", "istio-citadel", "istio-system", false},
|
||||
{"ParseJsonToK8sPod", testPodJSON, "", "Pod", "istio-galley-75bcd59768-hpt5t", "istio-system", false},
|
||||
{"ParseJsonToK8sService", testServiceJSON, "", "Service", "istio-pilot", "istio-system", false},
|
||||
{"ParseJsonError", testInvalidJSON, "", "", "", "", true},
|
||||
}
|
||||
|
||||
for _, tt := range parseJSONToK8sObjectTests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
k8sObj, err := ParseJSONToK8sObject([]byte(tt.objString))
|
||||
if err == nil {
|
||||
if tt.wantErr {
|
||||
t.Errorf("ParseJsonToK8sObject(%s): should be error", tt.desc)
|
||||
}
|
||||
k8sObjStr := k8sObj.YAMLDebugString()
|
||||
if k8sObj.Group != tt.wantGroup {
|
||||
t.Errorf("ParseJsonToK8sObject(%s): got group %s for k8s object %s, want %s", tt.desc, k8sObj.Group, k8sObjStr, tt.wantGroup)
|
||||
}
|
||||
if k8sObj.Kind != tt.wantKind {
|
||||
t.Errorf("ParseJsonToK8sObject(%s): got kind %s for k8s object %s, want %s", tt.desc, k8sObj.Kind, k8sObjStr, tt.wantKind)
|
||||
}
|
||||
if k8sObj.Name != tt.wantName {
|
||||
t.Errorf("ParseJsonToK8sObject(%s): got name %s for k8s object %s, want %s", tt.desc, k8sObj.Name, k8sObjStr, tt.wantName)
|
||||
}
|
||||
if k8sObj.Namespace != tt.wantNamespace {
|
||||
t.Errorf("ParseJsonToK8sObject(%s): got group %s for k8s object %s, want %s", tt.desc, k8sObj.Namespace, k8sObjStr, tt.wantNamespace)
|
||||
}
|
||||
} else if !tt.wantErr {
|
||||
t.Errorf("ParseJsonToK8sObject(%s): got unexpected error: %v", tt.desc, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseYAMLToK8sObject(t *testing.T) {
|
||||
testDeploymentYaml := `apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: istio-citadel
|
||||
namespace: istio-system
|
||||
labels:
|
||||
istio: citadel
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
istio: citadel
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
istio: citadel
|
||||
spec:
|
||||
containers:
|
||||
- name: citadel
|
||||
image: docker.io/istio/citadel:1.1.8
|
||||
args:
|
||||
- "--append-dns-names=true"
|
||||
- "--grpc-port=8060"
|
||||
- "--grpc-hostname=citadel"
|
||||
- "--citadel-storage-namespace=istio-system"
|
||||
- "--custom-dns-names=istio-pilot-service-account.istio-system:istio-pilot.istio-system"
|
||||
- "--monitoring-port=15014"
|
||||
- "--self-signed-ca=true"`
|
||||
|
||||
testPodYaml := `apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: istio-galley-75bcd59768-hpt5t
|
||||
namespace: istio-system
|
||||
labels:
|
||||
istio: galley
|
||||
spec:
|
||||
containers:
|
||||
- name: galley
|
||||
image: docker.io/istio/galley:1.1.8
|
||||
command:
|
||||
- "/usr/local/bin/galley"
|
||||
- server
|
||||
- "--meshConfigFile=/etc/mesh-config/mesh"
|
||||
- "--livenessProbeInterval=1s"
|
||||
- "--livenessProbePath=/healthliveness"
|
||||
- "--readinessProbePath=/healthready"
|
||||
- "--readinessProbeInterval=1s"
|
||||
- "--deployment-namespace=istio-system"
|
||||
- "--insecure=true"
|
||||
- "--validation-webhook-config-file"
|
||||
- "/etc/config/validatingwebhookconfiguration.yaml"
|
||||
- "--monitoringPort=15014"
|
||||
- "--log_output_level=default:info"
|
||||
ports:
|
||||
- containerPort: 443
|
||||
protocol: TCP
|
||||
- containerPort: 15014
|
||||
protocol: TCP
|
||||
- containerPort: 9901
|
||||
protocol: TCP`
|
||||
|
||||
testServiceYaml := `apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: pilot
|
||||
name: istio-pilot
|
||||
namespace: istio-system
|
||||
spec:
|
||||
clusterIP: 10.102.230.31
|
||||
ports:
|
||||
- name: grpc-xds
|
||||
port: 15010
|
||||
protocol: TCP
|
||||
targetPort: 15010
|
||||
- name: https-xds
|
||||
port: 15011
|
||||
protocol: TCP
|
||||
targetPort: 15011
|
||||
- name: http-legacy-discovery
|
||||
port: 8080
|
||||
protocol: TCP
|
||||
targetPort: 8080
|
||||
- name: http-monitoring
|
||||
port: 15014
|
||||
protocol: TCP
|
||||
targetPort: 15014
|
||||
selector:
|
||||
istio: pilot
|
||||
sessionAffinity: None
|
||||
type: ClusterIP`
|
||||
|
||||
parseYAMLToK8sObjectTests := []struct {
|
||||
desc string
|
||||
objString string
|
||||
wantGroup string
|
||||
wantKind string
|
||||
wantName string
|
||||
wantNamespace string
|
||||
}{
|
||||
{"ParseYamlToK8sDeployment", testDeploymentYaml, "apps", "Deployment", "istio-citadel", "istio-system"},
|
||||
{"ParseYamlToK8sPod", testPodYaml, "", "Pod", "istio-galley-75bcd59768-hpt5t", "istio-system"},
|
||||
{"ParseYamlToK8sService", testServiceYaml, "", "Service", "istio-pilot", "istio-system"},
|
||||
}
|
||||
|
||||
for _, tt := range parseYAMLToK8sObjectTests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
k8sObj, err := ParseYAMLToK8sObject([]byte(tt.objString))
|
||||
if err != nil {
|
||||
k8sObjStr := k8sObj.YAMLDebugString()
|
||||
if k8sObj.Group != tt.wantGroup {
|
||||
t.Errorf("ParseYAMLToK8sObject(%s): got group %s for k8s object %s, want %s", tt.desc, k8sObj.Group, k8sObjStr, tt.wantGroup)
|
||||
}
|
||||
if k8sObj.Group != tt.wantGroup {
|
||||
t.Errorf("ParseYAMLToK8sObject(%s): got kind %s for k8s object %s, want %s", tt.desc, k8sObj.Kind, k8sObjStr, tt.wantKind)
|
||||
}
|
||||
if k8sObj.Name != tt.wantName {
|
||||
t.Errorf("ParseYAMLToK8sObject(%s): got name %s for k8s object %s, want %s", tt.desc, k8sObj.Name, k8sObjStr, tt.wantName)
|
||||
}
|
||||
if k8sObj.Namespace != tt.wantNamespace {
|
||||
t.Errorf("ParseYAMLToK8sObject(%s): got group %s for k8s object %s, want %s", tt.desc, k8sObj.Namespace, k8sObjStr, tt.wantNamespace)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseK8sObjectsFromYAMLManifest(t *testing.T) {
|
||||
testDeploymentYaml := `apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: istio-citadel
|
||||
namespace: istio-system
|
||||
labels:
|
||||
istio: citadel
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
istio: citadel
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
istio: citadel
|
||||
spec:
|
||||
containers:
|
||||
- name: citadel
|
||||
image: docker.io/istio/citadel:1.1.8
|
||||
args:
|
||||
- "--append-dns-names=true"
|
||||
- "--grpc-port=8060"
|
||||
- "--grpc-hostname=citadel"
|
||||
- "--citadel-storage-namespace=istio-system"
|
||||
- "--custom-dns-names=istio-pilot-service-account.istio-system:istio-pilot.istio-system"
|
||||
- "--monitoring-port=15014"
|
||||
- "--self-signed-ca=true"`
|
||||
|
||||
testPodYaml := `apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: istio-galley-75bcd59768-hpt5t
|
||||
namespace: istio-system
|
||||
labels:
|
||||
istio: galley
|
||||
spec:
|
||||
containers:
|
||||
- name: galley
|
||||
image: docker.io/istio/galley:1.1.8
|
||||
command:
|
||||
- "/usr/local/bin/galley"
|
||||
- server
|
||||
- "--meshConfigFile=/etc/mesh-config/mesh"
|
||||
- "--livenessProbeInterval=1s"
|
||||
- "--livenessProbePath=/healthliveness"
|
||||
- "--readinessProbePath=/healthready"
|
||||
- "--readinessProbeInterval=1s"
|
||||
- "--deployment-namespace=istio-system"
|
||||
- "--insecure=true"
|
||||
- "--validation-webhook-config-file"
|
||||
- "/etc/config/validatingwebhookconfiguration.yaml"
|
||||
- "--monitoringPort=15014"
|
||||
- "--log_output_level=default:info"
|
||||
ports:
|
||||
- containerPort: 443
|
||||
protocol: TCP
|
||||
- containerPort: 15014
|
||||
protocol: TCP
|
||||
- containerPort: 9901
|
||||
protocol: TCP`
|
||||
|
||||
testServiceYaml := `apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: pilot
|
||||
name: istio-pilot
|
||||
namespace: istio-system
|
||||
spec:
|
||||
clusterIP: 10.102.230.31
|
||||
ports:
|
||||
- name: grpc-xds
|
||||
port: 15010
|
||||
protocol: TCP
|
||||
targetPort: 15010
|
||||
- name: https-xds
|
||||
port: 15011
|
||||
protocol: TCP
|
||||
targetPort: 15011
|
||||
- name: http-legacy-discovery
|
||||
port: 8080
|
||||
protocol: TCP
|
||||
targetPort: 8080
|
||||
- name: http-monitoring
|
||||
port: 15014
|
||||
protocol: TCP
|
||||
targetPort: 15014
|
||||
selector:
|
||||
istio: pilot
|
||||
sessionAffinity: None
|
||||
type: ClusterIP`
|
||||
|
||||
parseK8sObjectsFromYAMLManifestTests := []struct {
|
||||
desc string
|
||||
objsMap map[string]string
|
||||
}{
|
||||
{
|
||||
"FromHybridYAMLManifest",
|
||||
map[string]string{
|
||||
"Deployment:istio-system:istio-citadel": testDeploymentYaml,
|
||||
"Pod:istio-system:istio-galley-75bcd59768-hpt5t": testPodYaml,
|
||||
"Service:istio-system:istio-pilot": testServiceYaml,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range parseK8sObjectsFromYAMLManifestTests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
testManifestYaml := strings.Join([]string{testDeploymentYaml, testPodYaml, testServiceYaml}, YAMLSeparator)
|
||||
gotK8sObjs, err := ParseK8sObjectsFromYAMLManifest(testManifestYaml)
|
||||
if err != nil {
|
||||
gotK8sObjsMap := gotK8sObjs.ToMap()
|
||||
for objHash, want := range tt.objsMap {
|
||||
if gotObj, ok := gotK8sObjsMap[objHash]; ok {
|
||||
gotObjYaml := gotObj.YAMLDebugString()
|
||||
if !util.IsYAMLEqual(gotObjYaml, want) {
|
||||
t.Errorf("ParseK8sObjectsFromYAMLManifest(%s): got:\n%s\n\nwant:\n%s\nDiff:\n%s\n", tt.desc, gotObjYaml, want, util.YAMLDiff(gotObjYaml, want))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestK8sObject_Equal(t *testing.T) {
|
||||
obj1 := K8sObject{
|
||||
object: &unstructured.Unstructured{Object: map[string]any{
|
||||
"key": "value1",
|
||||
}},
|
||||
}
|
||||
obj2 := K8sObject{
|
||||
object: &unstructured.Unstructured{Object: map[string]any{
|
||||
"key": "value2",
|
||||
}},
|
||||
}
|
||||
cases := []struct {
|
||||
desc string
|
||||
o1 *K8sObject
|
||||
o2 *K8sObject
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
desc: "Equals",
|
||||
o1: &obj1,
|
||||
o2: &obj1,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
desc: "NotEquals",
|
||||
o1: &obj1,
|
||||
o2: &obj2,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
desc: "NilSource",
|
||||
o1: nil,
|
||||
o2: &obj2,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
desc: "NilDest",
|
||||
o1: &obj1,
|
||||
o2: nil,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
desc: "TwoNils",
|
||||
o1: nil,
|
||||
o2: nil,
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
res := tt.o1.Equal(tt.o2)
|
||||
if res != tt.want {
|
||||
t.Errorf("got %v, want: %v", res, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestK8sObject_ResolveK8sConflict(t *testing.T) {
|
||||
getK8sObject := func(ystr string) *K8sObject {
|
||||
o, err := ParseYAMLToK8sObject([]byte(ystr))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Ensure that json data is in sync.
|
||||
// Since the object was created using yaml, json is empty.
|
||||
// make sure the object json is set correctly.
|
||||
o.json, _ = o.JSON()
|
||||
return o
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
o1 *K8sObject
|
||||
o2 *K8sObject
|
||||
}{
|
||||
{
|
||||
desc: "not applicable kind",
|
||||
o1: getK8sObject(`
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: pilot
|
||||
name: istio-pilot
|
||||
namespace: istio-system
|
||||
spec:
|
||||
clusterIP: 10.102.230.31`),
|
||||
o2: getK8sObject(`
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: pilot
|
||||
name: istio-pilot
|
||||
namespace: istio-system
|
||||
spec:
|
||||
clusterIP: 10.102.230.31`),
|
||||
},
|
||||
{
|
||||
desc: "only minAvailable is set",
|
||||
o1: getK8sObject(`
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: zk-pdb
|
||||
spec:
|
||||
minAvailable: 2`),
|
||||
o2: getK8sObject(`
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: zk-pdb
|
||||
spec:
|
||||
minAvailable: 2`),
|
||||
},
|
||||
{
|
||||
desc: "only maxUnavailable is set",
|
||||
o1: getK8sObject(`
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: istio
|
||||
spec:
|
||||
maxUnavailable: 3`),
|
||||
o2: getK8sObject(`
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: istio
|
||||
spec:
|
||||
maxUnavailable: 3`),
|
||||
},
|
||||
{
|
||||
desc: "minAvailable and maxUnavailable are set to none zero values",
|
||||
o1: getK8sObject(`
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: istio
|
||||
spec:
|
||||
maxUnavailable: 50%
|
||||
minAvailable: 3`),
|
||||
o2: getK8sObject(`
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: istio
|
||||
spec:
|
||||
maxUnavailable: 50%`),
|
||||
},
|
||||
{
|
||||
desc: "both minAvailable and maxUnavailable are set default",
|
||||
o1: getK8sObject(`
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: istio
|
||||
spec:
|
||||
minAvailable: 0
|
||||
maxUnavailable: 0`),
|
||||
o2: getK8sObject(`
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: istio
|
||||
spec:
|
||||
maxUnavailable: 0
|
||||
minAvailable: 0`),
|
||||
},
|
||||
}
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
newObj := tt.o1.ResolveK8sConflict()
|
||||
if !newObj.Equal(tt.o2) {
|
||||
newObjjson, _ := newObj.JSON()
|
||||
wantedObjjson, _ := tt.o2.JSON()
|
||||
t.Errorf("Got: %s, want: %s", string(newObjjson), string(wantedObjjson))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
345
pkg/cmd/hgctl/helm/profile.go
Normal file
345
pkg/cmd/hgctl/helm/profile.go
Normal file
@@ -0,0 +1,345 @@
|
||||
// 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 helm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"istio.io/istio/operator/pkg/util"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
type InstallMode string
|
||||
|
||||
const (
|
||||
InstallK8s InstallMode = "k8s"
|
||||
InstallLocalK8s InstallMode = "local-k8s"
|
||||
InstallLocalDocker InstallMode = "local-docker"
|
||||
InstallLocal InstallMode = "local"
|
||||
)
|
||||
|
||||
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"`
|
||||
Controller ProfileController `json:"controller,omitempty"`
|
||||
Storage ProfileStorage `json:"storage,omitempty"`
|
||||
Values map[string]any `json:"values,omitempty"`
|
||||
Charts ProfileCharts `json:"charts,omitempty"`
|
||||
}
|
||||
|
||||
type ProfileGlobal struct {
|
||||
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)
|
||||
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, 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 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"`
|
||||
O11yEnabled bool `json:"o11YEnabled,omitempty"`
|
||||
}
|
||||
|
||||
func (p ProfileConsole) SetFlags(install InstallMode) ([]string, error) {
|
||||
sets := make([]string, 0)
|
||||
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 install == InstallK8s || install == InstallLocalK8s {
|
||||
if p.Replicas <= 0 {
|
||||
errs = append(errs, errors.New("console.replica need be large than zero"))
|
||||
}
|
||||
}
|
||||
|
||||
if install == InstallLocalDocker {
|
||||
if p.Port <= 0 {
|
||||
errs = append(errs, errors.New("console.port need be large than zero"))
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
type ProfileGateway struct {
|
||||
Replicas uint32 `json:"replicas,omitempty"`
|
||||
HttpPort uint32 `json:"httpPort,omitempty"`
|
||||
HttpsPort uint32 `json:"httpsPort,omitempty"`
|
||||
MetricsPort uint32 `json:"metricsPort,omitempty"`
|
||||
}
|
||||
|
||||
func (p ProfileGateway) SetFlags(install InstallMode) ([]string, error) {
|
||||
sets := make([]string, 0)
|
||||
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 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
|
||||
}
|
||||
|
||||
type ProfileController struct {
|
||||
Replicas uint32 `json:"replicas,omitempty"`
|
||||
}
|
||||
|
||||
func (p ProfileController) SetFlags(install InstallMode) ([]string, error) {
|
||||
sets := make([]string, 0)
|
||||
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 install == InstallK8s || install == InstallLocalK8s {
|
||||
if p.Replicas <= 0 {
|
||||
errs = append(errs, errors.New("controller.replica need be large than zero"))
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
type ProfileStorage struct {
|
||||
Url string `json:"url,omitempty"`
|
||||
Ns string `json:"ns,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
DataEncKey string `json:"DataEncKey,omitempty"`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type Chart struct {
|
||||
Url string `json:"url,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
type ProfileCharts struct {
|
||||
Higress Chart `json:"higress,omitempty"`
|
||||
Standalone Chart `json:"standalone,omitempty"`
|
||||
}
|
||||
|
||||
func (p ProfileCharts) Validate(install InstallMode) []error {
|
||||
errs := make([]error, 0)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (p *Profile) ValuesYaml() (string, error) {
|
||||
setFlags := make([]string, 0)
|
||||
// Get global setting
|
||||
globalFlags, _ := p.Global.SetFlags(p.Global.Install)
|
||||
setFlags = append(setFlags, globalFlags...)
|
||||
|
||||
// Get console setting
|
||||
consoleFlags, _ := p.Console.SetFlags(p.Global.Install)
|
||||
setFlags = append(setFlags, consoleFlags...)
|
||||
|
||||
// Get gateway setting
|
||||
gatewayFlags, _ := p.Gateway.SetFlags(p.Global.Install)
|
||||
setFlags = append(setFlags, gatewayFlags...)
|
||||
|
||||
// Get controller setting
|
||||
controllerFlags, _ := p.Controller.SetFlags(p.Global.Install)
|
||||
setFlags = append(setFlags, controllerFlags...)
|
||||
|
||||
valueOverlayYAML := ""
|
||||
if p.Values != nil {
|
||||
out, err := yaml.Marshal(p.Values)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
valueOverlayYAML = string(out)
|
||||
}
|
||||
|
||||
flagsYAML, err := overlaySetFlagValues("", setFlags)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// merge values and setFlags
|
||||
overlayYAML, err := util.OverlayYAML(flagsYAML, valueOverlayYAML)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return overlayYAML, nil
|
||||
}
|
||||
|
||||
func (p *Profile) IstioEnabled() bool {
|
||||
if (p.Global.Install == InstallK8s || p.Global.Install == InstallLocalK8s) && p.Global.EnableIstioAPI {
|
||||
return true
|
||||
}
|
||||
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)
|
||||
if len(errsGlobal) > 0 {
|
||||
errs = append(errs, errsGlobal...)
|
||||
}
|
||||
errsConsole := p.Console.Validate(p.Global.Install)
|
||||
if len(errsConsole) > 0 {
|
||||
errs = append(errs, errsConsole...)
|
||||
}
|
||||
errsGateway := p.Gateway.Validate(p.Global.Install)
|
||||
if len(errsGateway) > 0 {
|
||||
errs = append(errs, errsGateway...)
|
||||
}
|
||||
errsController := p.Controller.Validate(p.Global.Install)
|
||||
if len(errsController) > 0 {
|
||||
errs = append(errs, errsController...)
|
||||
}
|
||||
errsStorage := p.Storage.Validate(p.Global.Install)
|
||||
if len(errsStorage) > 0 {
|
||||
errs = append(errs, errsStorage...)
|
||||
}
|
||||
errsCharts := p.Charts.Validate(p.Global.Install)
|
||||
if len(errsCharts) > 0 {
|
||||
errs = append(errs, errsCharts...)
|
||||
}
|
||||
|
||||
if len(errs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return errors.New(ToString(errs, "\n"))
|
||||
}
|
||||
|
||||
// ToString returns a string representation of errors, with elements separated by separator string. Any nil errors in the
|
||||
// slice are skipped.
|
||||
func ToString(errors []error, separator string) string {
|
||||
var out string
|
||||
for i, e := range errors {
|
||||
if e == nil {
|
||||
continue
|
||||
}
|
||||
if i != 0 {
|
||||
out += separator
|
||||
}
|
||||
out += e.Error()
|
||||
}
|
||||
return out
|
||||
}
|
||||
685
pkg/cmd/hgctl/helm/render.go
Normal file
685
pkg/cmd/hgctl/helm/render.go
Normal file
@@ -0,0 +1,685 @@
|
||||
// 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 helm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/manifests"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/util"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"helm.sh/helm/v3/pkg/chart"
|
||||
"helm.sh/helm/v3/pkg/chart/loader"
|
||||
"helm.sh/helm/v3/pkg/chartutil"
|
||||
"helm.sh/helm/v3/pkg/cli"
|
||||
"helm.sh/helm/v3/pkg/downloader"
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultProfileName is the name of the default profile for installation.
|
||||
DefaultProfileName = "local-k8s"
|
||||
// DefaultProfileFilename is the name of the default profile yaml file for installation.
|
||||
DefaultProfileFilename = "local-k8s.yaml"
|
||||
// DefaultUninstallProfileName is the name of the default profile yaml file for uninstallation.
|
||||
DefaultUninstallProfileName = "local-k8s"
|
||||
|
||||
// ChartsSubdirName = "charts"
|
||||
profilesRoot = "profiles"
|
||||
|
||||
RepoLatestVersion = "latest"
|
||||
RepoChartIndexYamlHigressIndex = "higress"
|
||||
|
||||
YAMLSeparator = "\n---\n"
|
||||
NotesFileNameSuffix = ".txt"
|
||||
)
|
||||
|
||||
func LoadValues(profileName string, chartsDir string) (string, error) {
|
||||
path := strings.Join([]string{profilesRoot, builtinProfileToFilename(profileName)}, "/")
|
||||
by, err := fs.ReadFile(manifests.BuiltinOrDir(chartsDir), path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(by), nil
|
||||
}
|
||||
|
||||
func readProfiles(chartsDir string) (map[string]bool, error) {
|
||||
profiles := map[string]bool{}
|
||||
f := manifests.BuiltinOrDir(chartsDir)
|
||||
dir, err := fs.ReadDir(f, profilesRoot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, f := range dir {
|
||||
if f.Name() == "_all.yaml" {
|
||||
continue
|
||||
}
|
||||
trimmedString := strings.TrimSuffix(f.Name(), ".yaml")
|
||||
if f.Name() != trimmedString {
|
||||
profiles[trimmedString] = true
|
||||
}
|
||||
}
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
func builtinProfileToFilename(name string) string {
|
||||
if name == "" {
|
||||
return DefaultProfileFilename
|
||||
}
|
||||
return name + ".yaml"
|
||||
}
|
||||
|
||||
// stripPrefix removes the given prefix from prefix.
|
||||
func stripPrefix(path, prefix string) string {
|
||||
pl := len(strings.Split(prefix, "/"))
|
||||
pv := strings.Split(path, "/")
|
||||
return strings.Join(pv[pl:], "/")
|
||||
}
|
||||
|
||||
// ListProfiles list all the profiles.
|
||||
func ListProfiles(charts string) ([]string, error) {
|
||||
profiles, err := readProfiles(charts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return util.StringBoolMapToSlice(profiles), nil
|
||||
}
|
||||
|
||||
var DefaultFilters = []util.FilterFunc{
|
||||
util.LicenseFilter,
|
||||
util.FormatterFilter,
|
||||
util.SpaceFilter,
|
||||
}
|
||||
|
||||
// Renderer is responsible for rendering helm chart with new values.
|
||||
type Renderer interface {
|
||||
Init() error
|
||||
RenderManifest(valsYaml string) (string, error)
|
||||
SetVersion(version string)
|
||||
}
|
||||
|
||||
type RendererOptions struct {
|
||||
Name string
|
||||
Namespace string
|
||||
|
||||
// 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)
|
||||
|
||||
func WithName(name string) RendererOption {
|
||||
return func(opts *RendererOptions) {
|
||||
opts.Name = name
|
||||
}
|
||||
}
|
||||
|
||||
func WithNamespace(ns string) RendererOption {
|
||||
return func(opts *RendererOptions) {
|
||||
opts.Namespace = ns
|
||||
}
|
||||
}
|
||||
|
||||
func WithFS(f fs.FS) RendererOption {
|
||||
return func(opts *RendererOptions) {
|
||||
opts.FS = f
|
||||
}
|
||||
}
|
||||
|
||||
func WithDir(dir string) RendererOption {
|
||||
return func(opts *RendererOptions) {
|
||||
opts.Dir = dir
|
||||
}
|
||||
}
|
||||
|
||||
func WithVersion(version string) RendererOption {
|
||||
return func(opts *RendererOptions) {
|
||||
opts.Version = version
|
||||
}
|
||||
}
|
||||
|
||||
func WithRepoURL(repo string) RendererOption {
|
||||
return func(opts *RendererOptions) {
|
||||
opts.RepoURL = repo
|
||||
}
|
||||
}
|
||||
|
||||
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 *LocalChartRenderer) Init() error {
|
||||
fileNames, err := getFileNames(lr.Opts.FS, lr.Opts.Dir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("chart of component %s doesn't exist", lr.Opts.Name)
|
||||
}
|
||||
return fmt.Errorf("getFileNames err: %s", err)
|
||||
}
|
||||
var files []*loader.BufferedFile
|
||||
for _, fileName := range fileNames {
|
||||
data, err := fs.ReadFile(lr.Opts.FS, fileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ReadFile %s err: %s", fileName, err)
|
||||
}
|
||||
// todo:// explain why we need to do this
|
||||
name := util.StripPrefix(fileName, lr.Opts.Dir)
|
||||
file := &loader.BufferedFile{
|
||||
Name: name,
|
||||
Data: data,
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
newChart, err := loader.LoadFiles(files)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load chart of component %s err: %s", lr.Opts.Name, err)
|
||||
}
|
||||
lr.Chart = newChart
|
||||
lr.Started = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lr *LocalChartRenderer) RenderManifest(valsYaml string) (string, error) {
|
||||
if !lr.Started {
|
||||
return "", errors.New("LocalChartRenderer has not been init")
|
||||
}
|
||||
return renderManifest(valsYaml, lr.Chart, true, lr.Opts, DefaultFilters...)
|
||||
}
|
||||
|
||||
func (lr *LocalChartRenderer) SetVersion(version string) {
|
||||
lr.Opts.Version = version
|
||||
}
|
||||
|
||||
func NewLocalChartRenderer(opts ...RendererOption) (Renderer, error) {
|
||||
newOpts := &RendererOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(newOpts)
|
||||
}
|
||||
|
||||
if err := verifyRendererOptions(newOpts); err != nil {
|
||||
return nil, fmt.Errorf("verify err: %s", err)
|
||||
}
|
||||
return &LocalChartRenderer{
|
||||
Opts: newOpts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type RemoteRenderer struct {
|
||||
Opts *RendererOptions
|
||||
Chart *chart.Chart
|
||||
Started bool
|
||||
}
|
||||
|
||||
func (rr *RemoteRenderer) initChartPathOptions() *action.ChartPathOptions {
|
||||
return &action.ChartPathOptions{
|
||||
RepoURL: rr.Opts.RepoURL,
|
||||
Version: rr.Opts.Version,
|
||||
}
|
||||
}
|
||||
|
||||
func (rr *RemoteRenderer) Init() error {
|
||||
cpOpts := rr.initChartPathOptions()
|
||||
settings := cli.New()
|
||||
// using release name as chart name by default
|
||||
cp, err := locateChart(cpOpts, rr.Opts.Name, settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check chart dependencies to make sure all are present in /charts
|
||||
chartRequested, err := loader.Load(cp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := verifyInstallable(chartRequested); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rr.Chart = chartRequested
|
||||
rr.Started = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rr *RemoteRenderer) SetVersion(version string) {
|
||||
rr.Opts.Version = version
|
||||
}
|
||||
|
||||
func (rr *RemoteRenderer) RenderManifest(valsYaml string) (string, error) {
|
||||
if !rr.Started {
|
||||
return "", errors.New("RemoteRenderer has not been init")
|
||||
}
|
||||
return renderManifest(valsYaml, rr.Chart, false, rr.Opts, DefaultFilters...)
|
||||
}
|
||||
|
||||
func NewRemoteRenderer(opts ...RendererOption) (Renderer, error) {
|
||||
newOpts := &RendererOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(newOpts)
|
||||
}
|
||||
|
||||
return &RemoteRenderer{
|
||||
Opts: newOpts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func verifyRendererOptions(opts *RendererOptions) error {
|
||||
if opts.Name == "" {
|
||||
return errors.New("missing component name for Renderer")
|
||||
}
|
||||
if opts.Namespace == "" {
|
||||
return errors.New("missing component namespace for Renderer")
|
||||
}
|
||||
if opts.FS == nil {
|
||||
return errors.New("missing chart FS for Renderer")
|
||||
}
|
||||
if opts.Dir == "" {
|
||||
return errors.New("missing chart dir for Renderer")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// read all files recursively under root path from a certain local file system
|
||||
func getFileNames(f fs.FS, root string) ([]string, error) {
|
||||
var fileNames []string
|
||||
if err := fs.WalkDir(f, root, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
fileNames = append(fileNames, path)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fileNames, nil
|
||||
}
|
||||
|
||||
func verifyInstallable(cht *chart.Chart) error {
|
||||
typ := cht.Metadata.Type
|
||||
if typ == "" || typ == "application" {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s chart %s is not installable", typ, cht.Name())
|
||||
}
|
||||
|
||||
func renderManifest(valsYaml string, cht *chart.Chart, builtIn bool, opts *RendererOptions, filters ...util.FilterFunc) (string, error) {
|
||||
valsMap := make(map[string]any)
|
||||
if err := yaml.Unmarshal([]byte(valsYaml), &valsMap); err != nil {
|
||||
return "", fmt.Errorf("unmarshal failed err: %s", err)
|
||||
}
|
||||
RelOpts := chartutil.ReleaseOptions{
|
||||
Name: opts.Name,
|
||||
Namespace: opts.Namespace,
|
||||
}
|
||||
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 {
|
||||
return "", fmt.Errorf("ToRenderValues failed err: %s", err)
|
||||
}
|
||||
if builtIn {
|
||||
resVals["Values"].(chartutil.Values)["enabled"] = true
|
||||
}
|
||||
filesMap, err := engine.RenderWithClient(cht, resVals, opts.restConfig)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Render chart failed err: %s", err)
|
||||
}
|
||||
keys := make([]string, 0, len(filesMap))
|
||||
for key := range filesMap {
|
||||
// remove notation files such as Notes.txt
|
||||
if strings.HasSuffix(key, NotesFileNameSuffix) {
|
||||
continue
|
||||
}
|
||||
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 := filesMap[keys[i]]
|
||||
file = util.ApplyFilters(file, filters...)
|
||||
// ignore empty manifest
|
||||
if file == "" {
|
||||
continue
|
||||
}
|
||||
if !strings.HasSuffix(file, YAMLSeparator) {
|
||||
file += YAMLSeparator
|
||||
}
|
||||
builder.WriteString(file)
|
||||
}
|
||||
|
||||
// render CRD
|
||||
crdFiles := cht.CRDObjects()
|
||||
// Sort crd files by name to ensure stable manifest output
|
||||
sort.Slice(crdFiles, func(i, j int) bool { return crdFiles[i].Name < crdFiles[j].Name })
|
||||
for _, crdFile := range crdFiles {
|
||||
f := string(crdFile.File.Data)
|
||||
// add yaml separator if the rendered file doesn't have one at the end
|
||||
f = strings.TrimSpace(f) + "\n"
|
||||
if !strings.HasSuffix(f, YAMLSeparator) {
|
||||
f += YAMLSeparator
|
||||
}
|
||||
builder.WriteString(f)
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
// locateChart locate the target chart path by sequential orders:
|
||||
// 1. find local helm repository using "name-version.tgz" format
|
||||
// 2. using downloader to pull remote chart
|
||||
func locateChart(cpOpts *action.ChartPathOptions, name string, settings *cli.EnvSettings) (string, error) {
|
||||
name = strings.TrimSpace(name)
|
||||
version := strings.TrimSpace(cpOpts.Version)
|
||||
|
||||
// check if it's in Helm's chart cache
|
||||
// cacheName is hardcoded as format of helm. eg: grafana-6.31.1.tgz
|
||||
cacheName := name + "-" + cpOpts.Version + ".tgz"
|
||||
cachePath := path.Join(settings.RepositoryCache, cacheName)
|
||||
if _, err := os.Stat(cachePath); err == nil {
|
||||
abs, err := filepath.Abs(cachePath)
|
||||
if err != nil {
|
||||
return abs, err
|
||||
}
|
||||
if cpOpts.Verify {
|
||||
if _, err := downloader.VerifyChart(abs, cpOpts.Keyring); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return abs, nil
|
||||
}
|
||||
|
||||
dl := downloader.ChartDownloader{
|
||||
Out: os.Stdout,
|
||||
Keyring: cpOpts.Keyring,
|
||||
Getters: getter.All(settings),
|
||||
Options: []getter.Option{
|
||||
getter.WithPassCredentialsAll(cpOpts.PassCredentialsAll),
|
||||
getter.WithTLSClientConfig(cpOpts.CertFile, cpOpts.KeyFile, cpOpts.CaFile),
|
||||
getter.WithInsecureSkipVerifyTLS(cpOpts.InsecureSkipTLSverify),
|
||||
},
|
||||
RepositoryConfig: settings.RepositoryConfig,
|
||||
RepositoryCache: settings.RepositoryCache,
|
||||
}
|
||||
|
||||
if cpOpts.Verify {
|
||||
dl.Verify = downloader.VerifyAlways
|
||||
}
|
||||
if cpOpts.RepoURL != "" {
|
||||
chartURL, err := repo.FindChartInAuthAndTLSAndPassRepoURL(cpOpts.RepoURL, cpOpts.Username, cpOpts.Password, name, version,
|
||||
cpOpts.CertFile, cpOpts.KeyFile, cpOpts.CaFile, cpOpts.InsecureSkipTLSverify, cpOpts.PassCredentialsAll, getter.All(settings))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
name = chartURL
|
||||
|
||||
// Only pass the user/pass on when the user has said to or when the
|
||||
// location of the chart repo and the chart are the same domain.
|
||||
u1, err := url.Parse(cpOpts.RepoURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
u2, err := url.Parse(chartURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Host on URL (returned from url.Parse) contains the port if present.
|
||||
// This check ensures credentials are not passed between different
|
||||
// services on different ports.
|
||||
if cpOpts.PassCredentialsAll || (u1.Scheme == u2.Scheme && u1.Host == u2.Host) {
|
||||
dl.Options = append(dl.Options, getter.WithBasicAuth(cpOpts.Username, cpOpts.Password))
|
||||
} else {
|
||||
dl.Options = append(dl.Options, getter.WithBasicAuth("", ""))
|
||||
}
|
||||
} else {
|
||||
dl.Options = append(dl.Options, getter.WithBasicAuth(cpOpts.Username, cpOpts.Password))
|
||||
}
|
||||
|
||||
// if RepositoryCache doesn't exist, create it
|
||||
if err := os.MkdirAll(settings.RepositoryCache, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
filename, _, err := dl.DownloadTo(name, version, settings.RepositoryCache)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fileAbsPath, err := filepath.Abs(filename)
|
||||
if err != nil {
|
||||
return filename, err
|
||||
}
|
||||
return fileAbsPath, nil
|
||||
}
|
||||
|
||||
func ParseLatestVersion(repoUrl string, version string) (string, error) {
|
||||
|
||||
cpOpts := &action.ChartPathOptions{
|
||||
RepoURL: repoUrl,
|
||||
Version: version,
|
||||
}
|
||||
settings := cli.New()
|
||||
|
||||
indexURL, err := repo.ResolveReferenceURL(repoUrl, "index.yaml")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
u, err := url.Parse(repoUrl)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid chart URL format: %s", repoUrl)
|
||||
}
|
||||
|
||||
client, err := getter.All(settings).ByScheme(u.Scheme)
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not find protocol handler for: %s", u.Scheme)
|
||||
}
|
||||
|
||||
resp, err := client.Get(indexURL,
|
||||
getter.WithURL(cpOpts.RepoURL),
|
||||
getter.WithInsecureSkipVerifyTLS(cpOpts.InsecureSkipTLSverify),
|
||||
getter.WithTLSClientConfig(cpOpts.CertFile, cpOpts.KeyFile, cpOpts.CaFile),
|
||||
getter.WithBasicAuth(cpOpts.Username, cpOpts.Password),
|
||||
getter.WithPassCredentialsAll(cpOpts.PassCredentialsAll),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
index, err := io.ReadAll(resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
indexFile, err := loadIndex(index)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// get higress helm chart latest version
|
||||
if entries, ok := indexFile.Entries[RepoChartIndexYamlHigressIndex]; ok {
|
||||
return entries[0].AppVersion, nil
|
||||
}
|
||||
|
||||
return "", errors.New("can't find higress latest version")
|
||||
}
|
||||
|
||||
// loadIndex loads an index file and does minimal validity checking.
|
||||
//
|
||||
// The source parameter is only used for logging.
|
||||
// This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails.
|
||||
func loadIndex(data []byte) (*repo.IndexFile, error) {
|
||||
i := &repo.IndexFile{}
|
||||
if len(data) == 0 {
|
||||
return i, errors.New("empty index.yaml file")
|
||||
}
|
||||
if err := jsonOrYamlUnmarshal(data, i); err != nil {
|
||||
return i, err
|
||||
}
|
||||
for _, cvs := range i.Entries {
|
||||
for idx := len(cvs) - 1; idx >= 0; idx-- {
|
||||
if cvs[idx] == nil {
|
||||
continue
|
||||
}
|
||||
if cvs[idx].APIVersion == "" {
|
||||
cvs[idx].APIVersion = chart.APIVersionV1
|
||||
}
|
||||
if err := cvs[idx].Validate(); err != nil {
|
||||
cvs = append(cvs[:idx], cvs[idx+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
i.SortEntries()
|
||||
if i.APIVersion == "" {
|
||||
return i, errors.New("no API version specified")
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// jsonOrYamlUnmarshal unmarshals the given byte slice containing JSON or YAML
|
||||
// into the provided interface.
|
||||
//
|
||||
// It automatically detects whether the data is in JSON or YAML format by
|
||||
// checking its validity as JSON. If the data is valid JSON, it will use the
|
||||
// `encoding/json` package to unmarshal it. Otherwise, it will use the
|
||||
// `sigs.k8s.io/yaml` package to unmarshal the YAML data.
|
||||
func jsonOrYamlUnmarshal(b []byte, i interface{}) error {
|
||||
if json.Valid(b) {
|
||||
return json.Unmarshal(b, i)
|
||||
}
|
||||
return yaml.UnmarshalStrict(b, i)
|
||||
}
|
||||
548
pkg/cmd/hgctl/helm/tpath/tree.go
Normal file
548
pkg/cmd/hgctl/helm/tpath/tree.go
Normal file
@@ -0,0 +1,548 @@
|
||||
// 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 tpath
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/util"
|
||||
"gopkg.in/yaml.v2"
|
||||
yaml2 "sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// PathContext provides a means for traversing a tree towards the root.
|
||||
type PathContext struct {
|
||||
// Parent in the Parent of this PathContext.
|
||||
Parent *PathContext
|
||||
// KeyToChild is the key required to reach the child.
|
||||
KeyToChild any
|
||||
// Node is the actual Node in the data tree.
|
||||
Node any
|
||||
}
|
||||
|
||||
// String implements the Stringer interface.
|
||||
func (nc *PathContext) String() string {
|
||||
ret := "\n--------------- NodeContext ------------------\n"
|
||||
if nc.Parent != nil {
|
||||
ret += fmt.Sprintf("Parent.Node=\n%s\n", nc.Parent.Node)
|
||||
ret += fmt.Sprintf("KeyToChild=%v\n", nc.Parent.KeyToChild)
|
||||
}
|
||||
|
||||
ret += fmt.Sprintf("Node=\n%s\n", nc.Node)
|
||||
ret += "----------------------------------------------\n"
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// GetPathContext returns the PathContext for the Node which has the given path from root.
|
||||
// It returns false and no error if the given path is not found, or an error code in other error situations, like
|
||||
// a malformed path.
|
||||
// It also creates a tree of PathContexts during the traversal so that Parent nodes can be updated if required. This is
|
||||
// required when (say) appending to a list, where the parent list itself must be updated.
|
||||
func GetPathContext(root any, path util.Path, createMissing bool) (*PathContext, bool, error) {
|
||||
return getPathContext(&PathContext{Node: root}, path, path, createMissing)
|
||||
}
|
||||
|
||||
// WritePathContext writes the given value to the Node in the given PathContext.
|
||||
func WritePathContext(nc *PathContext, value any, merge bool) error {
|
||||
|
||||
if !util.IsValueNil(value) {
|
||||
return setPathContext(nc, value, merge)
|
||||
}
|
||||
|
||||
if nc.Parent == nil {
|
||||
return errors.New("cannot delete root element")
|
||||
}
|
||||
|
||||
switch {
|
||||
case isSliceOrPtrInterface(nc.Parent.Node):
|
||||
if err := util.DeleteFromSlicePtr(nc.Parent.Node, nc.Parent.KeyToChild.(int)); err != nil {
|
||||
return err
|
||||
}
|
||||
if isMapOrInterface(nc.Parent.Parent.Node) {
|
||||
return util.InsertIntoMap(nc.Parent.Parent.Node, nc.Parent.Parent.KeyToChild, nc.Parent.Node)
|
||||
}
|
||||
// TODO: The case of deleting a list.list.node element is not currently supported.
|
||||
return fmt.Errorf("cannot delete path: unsupported parent.parent type %T for delete", nc.Parent.Parent.Node)
|
||||
case util.IsMap(nc.Parent.Node):
|
||||
return util.DeleteFromMap(nc.Parent.Node, nc.Parent.KeyToChild)
|
||||
default:
|
||||
}
|
||||
return fmt.Errorf("cannot delete path: unsupported parent type %T for delete", nc.Parent.Node)
|
||||
}
|
||||
|
||||
// WriteNode writes value to the tree in root at the given path, creating any required missing internal nodes in path.
|
||||
func WriteNode(root any, path util.Path, value any) error {
|
||||
pc, _, err := getPathContext(&PathContext{Node: root}, path, path, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return WritePathContext(pc, value, false)
|
||||
}
|
||||
|
||||
// MergeNode merges value to the tree in root at the given path, creating any required missing internal nodes in path.
|
||||
func MergeNode(root any, path util.Path, value any) error {
|
||||
pc, _, err := getPathContext(&PathContext{Node: root}, path, path, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return WritePathContext(pc, value, true)
|
||||
}
|
||||
|
||||
// Find returns the value at path from the given tree, or false if the path does not exist.
|
||||
// It behaves differently from GetPathContext in that it never creates map entries at the leaf and does not provide
|
||||
// a way to mutate the parent of the found node.
|
||||
func Find(inputTree map[string]any, path util.Path) (any, bool, error) {
|
||||
if len(path) == 0 {
|
||||
return nil, false, fmt.Errorf("path is empty")
|
||||
}
|
||||
node, found := find(inputTree, path)
|
||||
return node, found, nil
|
||||
}
|
||||
|
||||
// Delete sets value at path of input untyped tree to nil
|
||||
func Delete(root map[string]any, path util.Path) (bool, error) {
|
||||
pc, _, err := getPathContext(&PathContext{Node: root}, path, path, false)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, WritePathContext(pc, nil, false)
|
||||
}
|
||||
|
||||
// getPathContext is the internal implementation of GetPathContext.
|
||||
// If createMissing is true, it creates any missing map (but NOT list) path entries in root.
|
||||
func getPathContext(nc *PathContext, fullPath, remainPath util.Path, createMissing bool) (*PathContext, bool, error) {
|
||||
if len(remainPath) == 0 {
|
||||
return nc, true, nil
|
||||
}
|
||||
pe := remainPath[0]
|
||||
|
||||
if nc.Node == nil {
|
||||
if !createMissing {
|
||||
return nil, false, fmt.Errorf("node %s is zero", pe)
|
||||
}
|
||||
if util.IsNPathElement(pe) || util.IsKVPathElement(pe) {
|
||||
nc.Node = []any{}
|
||||
} else {
|
||||
nc.Node = make(map[string]any)
|
||||
}
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(nc.Node)
|
||||
if v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
|
||||
v = v.Elem()
|
||||
}
|
||||
ncNode := v.Interface()
|
||||
|
||||
// For list types, we need a key to identify the selected list item. This can be either a value key of the
|
||||
// form :matching_value in the case of a leaf list, or a matching key:value in the case of a non-leaf list.
|
||||
if lst, ok := ncNode.([]any); ok {
|
||||
// If the path element has the form [N], a list element is being selected by index. Return the element at index
|
||||
// N if it exists.
|
||||
if util.IsNPathElement(pe) {
|
||||
idx, err := util.PathN(pe)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("path %s, index %s: %s", fullPath, pe, err)
|
||||
}
|
||||
var foundNode any
|
||||
if idx >= len(lst) || idx < 0 {
|
||||
if !createMissing {
|
||||
return nil, false, fmt.Errorf("index %d exceeds list length %d at path %s", idx, len(lst), remainPath)
|
||||
}
|
||||
idx = len(lst)
|
||||
foundNode = make(map[string]any)
|
||||
} else {
|
||||
foundNode = lst[idx]
|
||||
}
|
||||
nn := &PathContext{
|
||||
Parent: nc,
|
||||
Node: foundNode,
|
||||
}
|
||||
nc.KeyToChild = idx
|
||||
return getPathContext(nn, fullPath, remainPath[1:], createMissing)
|
||||
}
|
||||
|
||||
// Otherwise the path element must have form [key:value]. In this case, go through all list elements, which
|
||||
// must have map type, and try to find one which has a matching key:value.
|
||||
for idx, le := range lst {
|
||||
// non-leaf list, expect to match item by key:value.
|
||||
if lm, ok := le.(map[any]any); ok {
|
||||
k, v, err := util.PathKV(pe)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("path %s: %s", fullPath, err)
|
||||
}
|
||||
if stringsEqual(lm[k], v) {
|
||||
nn := &PathContext{
|
||||
Parent: nc,
|
||||
Node: lm,
|
||||
}
|
||||
nc.KeyToChild = idx
|
||||
nn.KeyToChild = k
|
||||
if len(remainPath) == 1 {
|
||||
return nn, true, nil
|
||||
}
|
||||
return getPathContext(nn, fullPath, remainPath[1:], createMissing)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// repeat of the block above for the case where tree unmarshals to map[string]interface{}. There doesn't
|
||||
// seem to be a way to merge this case into the above block.
|
||||
if lm, ok := le.(map[string]any); ok {
|
||||
k, v, err := util.PathKV(pe)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("path %s: %s", fullPath, err)
|
||||
}
|
||||
if stringsEqual(lm[k], v) {
|
||||
nn := &PathContext{
|
||||
Parent: nc,
|
||||
Node: lm,
|
||||
}
|
||||
nc.KeyToChild = idx
|
||||
nn.KeyToChild = k
|
||||
if len(remainPath) == 1 {
|
||||
return nn, true, nil
|
||||
}
|
||||
return getPathContext(nn, fullPath, remainPath[1:], createMissing)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// leaf list, expect path element [V], match based on value V.
|
||||
v, err := util.PathV(pe)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("path %s: %s", fullPath, err)
|
||||
}
|
||||
if matchesRegex(v, le) {
|
||||
nn := &PathContext{
|
||||
Parent: nc,
|
||||
Node: le,
|
||||
}
|
||||
nc.KeyToChild = idx
|
||||
return getPathContext(nn, fullPath, remainPath[1:], createMissing)
|
||||
}
|
||||
}
|
||||
return nil, false, fmt.Errorf("path %s: element %s not found", fullPath, pe)
|
||||
}
|
||||
|
||||
if util.IsMap(ncNode) {
|
||||
var nn any
|
||||
if m, ok := ncNode.(map[any]any); ok {
|
||||
nn, ok = m[pe]
|
||||
if !ok {
|
||||
// remainPath == 1 means the patch is creation of a new leaf.
|
||||
if createMissing || len(remainPath) == 1 {
|
||||
m[pe] = make(map[any]any)
|
||||
nn = m[pe]
|
||||
} else {
|
||||
return nil, false, fmt.Errorf("path not found at element %s in path %s", pe, fullPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
if reflect.ValueOf(ncNode).IsNil() {
|
||||
ncNode = make(map[string]any)
|
||||
nc.Node = ncNode
|
||||
}
|
||||
if m, ok := ncNode.(map[string]any); ok {
|
||||
nn, ok = m[pe]
|
||||
if !ok {
|
||||
// remainPath == 1 means the patch is creation of a new leaf.
|
||||
if createMissing || len(remainPath) == 1 {
|
||||
nextElementNPath := len(remainPath) > 1 && util.IsNPathElement(remainPath[1])
|
||||
if nextElementNPath {
|
||||
m[pe] = make([]any, 0)
|
||||
} else {
|
||||
m[pe] = make(map[string]any)
|
||||
}
|
||||
nn = m[pe]
|
||||
} else {
|
||||
return nil, false, fmt.Errorf("path not found at element %s in path %s", pe, fullPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
npc := &PathContext{
|
||||
Parent: nc,
|
||||
Node: nn,
|
||||
}
|
||||
// for slices, use the address so that the slice can be mutated.
|
||||
if util.IsSlice(nn) {
|
||||
npc.Node = &nn
|
||||
}
|
||||
nc.KeyToChild = pe
|
||||
return getPathContext(npc, fullPath, remainPath[1:], createMissing)
|
||||
}
|
||||
|
||||
return nil, false, fmt.Errorf("leaf type %T in non-leaf Node %s", nc.Node, remainPath)
|
||||
}
|
||||
|
||||
// setPathContext writes the given value to the Node in the given PathContext,
|
||||
// enlarging all PathContext lists to ensure all indexes are valid.
|
||||
func setPathContext(nc *PathContext, value any, merge bool) error {
|
||||
processParent, err := setValueContext(nc, value, merge)
|
||||
if err != nil || !processParent {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the path included insertions, process them now
|
||||
if nc.Parent.Parent == nil {
|
||||
return nil
|
||||
}
|
||||
return setPathContext(nc.Parent, nc.Parent.Node, false) // note: tail recursive
|
||||
}
|
||||
|
||||
// setValueContext writes the given value to the Node in the given PathContext.
|
||||
// If setting the value requires growing the final slice, grows it.
|
||||
func setValueContext(nc *PathContext, value any, merge bool) (bool, error) {
|
||||
if nc.Parent == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
vv, mapFromString := tryToUnmarshalStringToYAML(value)
|
||||
|
||||
switch parentNode := nc.Parent.Node.(type) {
|
||||
case *any:
|
||||
switch vParentNode := (*parentNode).(type) {
|
||||
case []any:
|
||||
idx := nc.Parent.KeyToChild.(int)
|
||||
if idx == -1 {
|
||||
// Treat -1 as insert-at-end of list
|
||||
idx = len(vParentNode)
|
||||
}
|
||||
|
||||
if idx >= len(vParentNode) {
|
||||
newElements := make([]any, idx-len(vParentNode)+1)
|
||||
vParentNode = append(vParentNode, newElements...)
|
||||
*parentNode = vParentNode
|
||||
}
|
||||
|
||||
merged, err := mergeConditional(vv, nc.Node, merge)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
vParentNode[idx] = merged
|
||||
nc.Node = merged
|
||||
default:
|
||||
return false, fmt.Errorf("don't know about vtype %T", vParentNode)
|
||||
}
|
||||
case map[string]any:
|
||||
key := nc.Parent.KeyToChild.(string)
|
||||
|
||||
// Update is treated differently depending on whether the value is a scalar or map type. If scalar,
|
||||
// insert a new element into the terminal node, otherwise replace the terminal node with the new subtree.
|
||||
if ncNode, ok := nc.Node.(*any); ok && !mapFromString {
|
||||
switch vNcNode := (*ncNode).(type) {
|
||||
case []any:
|
||||
switch vv.(type) {
|
||||
case map[string]any:
|
||||
// the vv is a map, and the node is a slice
|
||||
mergedValue := append(vNcNode, vv)
|
||||
parentNode[key] = mergedValue
|
||||
case *any:
|
||||
merged, err := mergeConditional(vv, vNcNode, merge)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
parentNode[key] = merged
|
||||
nc.Node = merged
|
||||
default:
|
||||
// the vv is an basic JSON type (int, float, string, bool)
|
||||
vv = append(vNcNode, vv)
|
||||
parentNode[key] = vv
|
||||
nc.Node = vv
|
||||
}
|
||||
default:
|
||||
return false, fmt.Errorf("don't know about vnc type %T", vNcNode)
|
||||
}
|
||||
} else {
|
||||
// For map passed as string type, the root is the new key.
|
||||
if mapFromString {
|
||||
if err := util.DeleteFromMap(nc.Parent.Node, nc.Parent.KeyToChild); err != nil {
|
||||
return false, err
|
||||
}
|
||||
vm := vv.(map[string]any)
|
||||
newKey := getTreeRoot(vm)
|
||||
return false, util.InsertIntoMap(nc.Parent.Node, newKey, vm[newKey])
|
||||
}
|
||||
parentNode[key] = vv
|
||||
nc.Node = vv
|
||||
}
|
||||
// TODO `map[interface{}]interface{}` is used by tests in operator/cmd/mesh, we should add our own tests
|
||||
case map[any]any:
|
||||
key := nc.Parent.KeyToChild.(string)
|
||||
parentNode[key] = vv
|
||||
nc.Node = vv
|
||||
default:
|
||||
return false, fmt.Errorf("don't know about type %T", parentNode)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// mergeConditional returns a merge of newVal and originalVal if merge is true, otherwise it returns newVal.
|
||||
func mergeConditional(newVal, originalVal any, merge bool) (any, error) {
|
||||
if !merge || util.IsValueNilOrDefault(originalVal) {
|
||||
return newVal, nil
|
||||
}
|
||||
newS, err := yaml.Marshal(newVal)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if util.IsYAMLEmpty(string(newS)) {
|
||||
return originalVal, nil
|
||||
}
|
||||
originalS, err := yaml.Marshal(originalVal)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if util.IsYAMLEmpty(string(originalS)) {
|
||||
return newVal, nil
|
||||
}
|
||||
|
||||
mergedS, err := util.OverlayYAML(string(originalS), string(newS))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if util.IsMap(originalVal) {
|
||||
// For JSON compatibility
|
||||
out := make(map[string]any)
|
||||
if err := yaml.Unmarshal([]byte(mergedS), &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
// For scalars and slices, copy the type
|
||||
out := originalVal
|
||||
if err := yaml.Unmarshal([]byte(mergedS), &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// find returns the value at path from the given tree, or false if the path does not exist.
|
||||
func find(treeNode any, path util.Path) (any, bool) {
|
||||
if len(path) == 0 || treeNode == nil {
|
||||
return nil, false
|
||||
}
|
||||
switch nt := treeNode.(type) {
|
||||
case map[any]any:
|
||||
val := nt[path[0]]
|
||||
if val == nil {
|
||||
return nil, false
|
||||
}
|
||||
if len(path) == 1 {
|
||||
return val, true
|
||||
}
|
||||
return find(val, path[1:])
|
||||
case map[string]any:
|
||||
val := nt[path[0]]
|
||||
if val == nil {
|
||||
return nil, false
|
||||
}
|
||||
if len(path) == 1 {
|
||||
return val, true
|
||||
}
|
||||
return find(val, path[1:])
|
||||
case []any:
|
||||
idx, err := strconv.Atoi(path[0])
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
if idx >= len(nt) {
|
||||
return nil, false
|
||||
}
|
||||
val := nt[idx]
|
||||
return find(val, path[1:])
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
// stringsEqual reports whether the string representations of a and b are equal. a and b may have different types.
|
||||
func stringsEqual(a, b any) bool {
|
||||
return fmt.Sprint(a) == fmt.Sprint(b)
|
||||
}
|
||||
|
||||
// matchesRegex reports whether str regex matches pattern.
|
||||
func matchesRegex(pattern, str any) bool {
|
||||
match, err := regexp.MatchString(fmt.Sprint(pattern), fmt.Sprint(str))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return match
|
||||
}
|
||||
|
||||
// isSliceOrPtrInterface reports whether v is a slice, a ptr to slice or interface to slice.
|
||||
func isSliceOrPtrInterface(v any) bool {
|
||||
vv := reflect.ValueOf(v)
|
||||
if vv.Kind() == reflect.Ptr {
|
||||
vv = vv.Elem()
|
||||
}
|
||||
if vv.Kind() == reflect.Interface {
|
||||
vv = vv.Elem()
|
||||
}
|
||||
return vv.Kind() == reflect.Slice
|
||||
}
|
||||
|
||||
// isMapOrInterface reports whether v is a map, or interface to a map.
|
||||
func isMapOrInterface(v any) bool {
|
||||
vv := reflect.ValueOf(v)
|
||||
if vv.Kind() == reflect.Interface {
|
||||
vv = vv.Elem()
|
||||
}
|
||||
return vv.Kind() == reflect.Map
|
||||
}
|
||||
|
||||
// tryToUnmarshalStringToYAML tries to unmarshal something that may be a YAML list or map into a structure. If not
|
||||
// possible, returns original scalar value.
|
||||
func tryToUnmarshalStringToYAML(s any) (any, bool) {
|
||||
// If value type is a string it could either be a literal string or a map type passed as a string. Try to unmarshal
|
||||
// to discover it's the latter.
|
||||
vv := s
|
||||
|
||||
if reflect.TypeOf(vv).Kind() == reflect.String {
|
||||
sv := strings.Split(vv.(string), "\n")
|
||||
// Need to be careful not to transform string literals into maps unless they really are maps, since scalar handling
|
||||
// is different for inserts.
|
||||
if len(sv) == 1 && strings.Contains(s.(string), ": ") ||
|
||||
len(sv) > 1 && strings.Contains(s.(string), ":") {
|
||||
nv := make(map[string]any)
|
||||
if err := json.Unmarshal([]byte(vv.(string)), &nv); err == nil {
|
||||
// treat JSON as string
|
||||
return vv, false
|
||||
}
|
||||
if err := yaml2.Unmarshal([]byte(vv.(string)), &nv); err == nil {
|
||||
return nv, true
|
||||
}
|
||||
}
|
||||
}
|
||||
// looks like a literal or failed unmarshal, return original type.
|
||||
return vv, false
|
||||
}
|
||||
|
||||
// getTreeRoot returns the first key found in m. It assumes a single root tree.
|
||||
func getTreeRoot(m map[string]any) string {
|
||||
for k := range m {
|
||||
return k
|
||||
}
|
||||
return ""
|
||||
}
|
||||
843
pkg/cmd/hgctl/helm/tpath/tree_test.go
Normal file
843
pkg/cmd/hgctl/helm/tpath/tree_test.go
Normal file
@@ -0,0 +1,843 @@
|
||||
// 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 tpath
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/util"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
func TestWritePathContext(t *testing.T) {
|
||||
rootYAML := `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v1
|
||||
- name: n2
|
||||
list:
|
||||
- v1
|
||||
- v2
|
||||
- v3_regex
|
||||
`
|
||||
tests := []struct {
|
||||
desc string
|
||||
path string
|
||||
value any
|
||||
want string
|
||||
wantFound bool
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
desc: "AddListEntry",
|
||||
path: `a.b.[name:n2].list`,
|
||||
value: `foo`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v1
|
||||
- name: n2
|
||||
list:
|
||||
- v1
|
||||
- v2
|
||||
- v3_regex
|
||||
- foo
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "ModifyListEntryValue",
|
||||
path: `a.b.[name:n1].value`,
|
||||
value: `v2`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v2
|
||||
- list:
|
||||
- v1
|
||||
- v2
|
||||
- v3_regex
|
||||
name: n2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "ModifyListEntryValueQuoted",
|
||||
path: `a.b.[name:n1].value`,
|
||||
value: `v2`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: "n1"
|
||||
value: v2
|
||||
- list:
|
||||
- v1
|
||||
- v2
|
||||
- v3_regex
|
||||
name: n2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "ModifyListEntry",
|
||||
path: `a.b.[name:n2].list.[:v2]`,
|
||||
value: `v3`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v1
|
||||
- list:
|
||||
- v1
|
||||
- v3
|
||||
- v3_regex
|
||||
name: n2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "ModifyListEntryMapValue",
|
||||
path: `a.b.[name:n2]`,
|
||||
value: `name: n2
|
||||
list:
|
||||
- nk1: nv1
|
||||
- nk2: nv2`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v1
|
||||
- name: n2
|
||||
list:
|
||||
- nk1: nv1
|
||||
- nk2: nv2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "ModifyNthListEntry",
|
||||
path: `a.b.[1].list.[:v2]`,
|
||||
value: `v-the-second`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v1
|
||||
- list:
|
||||
- v1
|
||||
- v-the-second
|
||||
- v3_regex
|
||||
name: n2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "ModifyNthLeafListEntry",
|
||||
path: `a.b.[1].list.[2]`,
|
||||
value: `v-the-third`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v1
|
||||
- list:
|
||||
- v1
|
||||
- v2
|
||||
- v-the-third
|
||||
name: n2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "ModifyListEntryValueDotless",
|
||||
path: `a.b[name:n1].value`,
|
||||
value: `v2`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v2
|
||||
- list:
|
||||
- v1
|
||||
- v2
|
||||
- v3_regex
|
||||
name: n2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "DeleteListEntry",
|
||||
path: `a.b.[name:n1]`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- list:
|
||||
- v1
|
||||
- v2
|
||||
- v3_regex
|
||||
name: n2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "DeleteListEntryValue",
|
||||
path: `a.b.[name:n2].list.[:v2]`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v1
|
||||
- list:
|
||||
- v1
|
||||
- v3_regex
|
||||
name: n2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "DeleteListEntryIndex",
|
||||
path: `a.b.[name:n2].list.[1]`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v1
|
||||
- list:
|
||||
- v1
|
||||
- v3_regex
|
||||
name: n2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "DeleteListEntryValueRegex",
|
||||
path: `a.b.[name:n2].list.[:v3]`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v1
|
||||
- list:
|
||||
- v1
|
||||
- v2
|
||||
name: n2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "DeleteListLeafEntryBogusIndex",
|
||||
path: `a.b.[name:n2].list.[-200]`,
|
||||
wantFound: false,
|
||||
wantErr: `path a.b.[name:n2].list.[-200]: element [-200] not found`,
|
||||
},
|
||||
{
|
||||
desc: "DeleteListEntryBogusIndex",
|
||||
path: `a.b.[1000000].list.[:v2]`,
|
||||
wantFound: false,
|
||||
wantErr: `index 1000000 exceeds list length 2 at path [1000000].list.[:v2]`,
|
||||
},
|
||||
{
|
||||
desc: "AddMapEntry",
|
||||
path: `a.new_key`,
|
||||
value: `new_val`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v1
|
||||
- name: n2
|
||||
list:
|
||||
- v1
|
||||
- v2
|
||||
- v3_regex
|
||||
new_key: new_val
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "AddMapEntryMapValue",
|
||||
path: `a.new_key`,
|
||||
value: `new_key:
|
||||
nk1:
|
||||
nk2: nv2`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v1
|
||||
- name: n2
|
||||
list:
|
||||
- v1
|
||||
- v2
|
||||
- v3_regex
|
||||
new_key:
|
||||
nk1:
|
||||
nk2: nv2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "ModifyMapEntryMapValue",
|
||||
path: `a.b`,
|
||||
value: `nk1:
|
||||
nk2: nv2`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
nk1:
|
||||
nk2: nv2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "DeleteMapEntry",
|
||||
path: `a.b`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a: {}
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "path not found",
|
||||
path: `a.c.[name:n2].list.[:v3]`,
|
||||
wantFound: false,
|
||||
wantErr: `path not found at element c in path a.c.[name:n2].list.[:v3]`,
|
||||
},
|
||||
{
|
||||
desc: "error key",
|
||||
path: `a.b.[].list`,
|
||||
wantFound: false,
|
||||
wantErr: `path a.b.[].list: [] is not a valid key:value path element`,
|
||||
},
|
||||
{
|
||||
desc: "invalid index",
|
||||
path: `a.c.[n2].list.[:v3]`,
|
||||
wantFound: false,
|
||||
wantErr: `path not found at element c in path a.c.[n2].list.[:v3]`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
root := make(map[string]any)
|
||||
if err := yaml.Unmarshal([]byte(rootYAML), &root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pc, gotFound, gotErr := GetPathContext(root, util.PathFromString(tt.path), false)
|
||||
if gotErr, wantErr := errToString(gotErr), tt.wantErr; gotErr != wantErr {
|
||||
t.Fatalf("GetPathContext(%s): gotErr:%s, wantErr:%s", tt.desc, gotErr, wantErr)
|
||||
}
|
||||
if gotFound != tt.wantFound {
|
||||
t.Fatalf("GetPathContext(%s): gotFound:%v, wantFound:%v", tt.desc, gotFound, tt.wantFound)
|
||||
}
|
||||
if tt.wantErr != "" || !tt.wantFound {
|
||||
if tt.want != "" {
|
||||
t.Error("tt.want is set but never checked")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
err := WritePathContext(pc, tt.value, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
gotYAML := util.ToYAML(root)
|
||||
diff := util.YAMLDiff(gotYAML, tt.want)
|
||||
if diff != "" {
|
||||
t.Errorf("%s: (got:-, want:+):\n%s\n", tt.desc, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteNode(t *testing.T) {
|
||||
testTreeYAML := `
|
||||
a:
|
||||
b:
|
||||
c: val1
|
||||
list1:
|
||||
- i1: val1
|
||||
- i2: val2
|
||||
- i3a: key1
|
||||
i3b:
|
||||
list2:
|
||||
- i1: val1
|
||||
- i2: val2
|
||||
- i3a: key1
|
||||
i3b:
|
||||
i1: va11
|
||||
`
|
||||
tests := []struct {
|
||||
desc string
|
||||
baseYAML string
|
||||
path string
|
||||
value string
|
||||
want string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
desc: "insert empty",
|
||||
path: "a.b.c",
|
||||
value: "val1",
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
c: val1
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "overwrite",
|
||||
baseYAML: testTreeYAML,
|
||||
path: "a.b.c",
|
||||
value: "val2",
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
c: val2
|
||||
list1:
|
||||
- i1: val1
|
||||
- i2: val2
|
||||
- i3a: key1
|
||||
i3b:
|
||||
list2:
|
||||
- i1: val1
|
||||
- i2: val2
|
||||
- i3a: key1
|
||||
i3b:
|
||||
i1: va11
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "partial create",
|
||||
baseYAML: testTreeYAML,
|
||||
path: "a.b.d",
|
||||
value: "val3",
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
c: val1
|
||||
d: val3
|
||||
list1:
|
||||
- i1: val1
|
||||
- i2: val2
|
||||
- i3a: key1
|
||||
i3b:
|
||||
list2:
|
||||
- i1: val1
|
||||
- i2: val2
|
||||
- i3a: key1
|
||||
i3b:
|
||||
i1: va11
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "list keys",
|
||||
baseYAML: testTreeYAML,
|
||||
path: "a.b.list1.[i3a:key1].i3b.list2.[i3a:key1].i3b.i1",
|
||||
value: "val2",
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
c: val1
|
||||
list1:
|
||||
- i1: val1
|
||||
- i2: val2
|
||||
- i3a: key1
|
||||
i3b:
|
||||
list2:
|
||||
- i1: val1
|
||||
- i2: val2
|
||||
- i3a: key1
|
||||
i3b:
|
||||
i1: val2
|
||||
`,
|
||||
},
|
||||
// For https://github.com/istio/istio/issues/20950
|
||||
{
|
||||
desc: "with initial list",
|
||||
baseYAML: `
|
||||
components:
|
||||
ingressGateways:
|
||||
- enabled: true
|
||||
`,
|
||||
path: "components.ingressGateways[0].enabled",
|
||||
value: "false",
|
||||
want: `
|
||||
components:
|
||||
ingressGateways:
|
||||
- enabled: "false"
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "no initial list",
|
||||
baseYAML: "",
|
||||
path: "components.ingressGateways[0].enabled",
|
||||
value: "false",
|
||||
want: `
|
||||
components:
|
||||
ingressGateways:
|
||||
- enabled: "false"
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "no initial list for entry",
|
||||
baseYAML: `
|
||||
a: {}
|
||||
`,
|
||||
path: "a.list.[0]",
|
||||
value: "v1",
|
||||
want: `
|
||||
a:
|
||||
list:
|
||||
- v1
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "ExtendNthLeafListEntry",
|
||||
baseYAML: `
|
||||
a:
|
||||
list:
|
||||
- v1
|
||||
`,
|
||||
path: `a.list.[1]`,
|
||||
value: `v2`,
|
||||
want: `
|
||||
a:
|
||||
list:
|
||||
- v1
|
||||
- v2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "ExtendLeafListEntryLargeIndex",
|
||||
baseYAML: `
|
||||
a:
|
||||
list:
|
||||
- v1
|
||||
`,
|
||||
path: `a.list.[999]`,
|
||||
value: `v2`,
|
||||
want: `
|
||||
a:
|
||||
list:
|
||||
- v1
|
||||
- v2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "ExtendLeafListEntryNegativeIndex",
|
||||
baseYAML: `
|
||||
a:
|
||||
list:
|
||||
- v1
|
||||
`,
|
||||
path: `a.list.[-1]`,
|
||||
value: `v2`,
|
||||
want: `
|
||||
a:
|
||||
list:
|
||||
- v1
|
||||
- v2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "ExtendNthListEntry",
|
||||
baseYAML: `
|
||||
a:
|
||||
list:
|
||||
- name: foo
|
||||
`,
|
||||
path: `a.list.[1].name`,
|
||||
value: `bar`,
|
||||
want: `
|
||||
a:
|
||||
list:
|
||||
- name: foo
|
||||
- name: bar
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
root := make(map[string]any)
|
||||
if tt.baseYAML != "" {
|
||||
if err := yaml.Unmarshal([]byte(tt.baseYAML), &root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
p := util.PathFromString(tt.path)
|
||||
err := WriteNode(root, p, tt.value)
|
||||
if gotErr, wantErr := errToString(err), tt.wantErr; gotErr != wantErr {
|
||||
t.Errorf("%s: gotErr:%s, wantErr:%s", tt.desc, gotErr, wantErr)
|
||||
return
|
||||
}
|
||||
if got, want := util.ToYAML(root), tt.want; err == nil && util.YAMLDiff(got, want) != "" {
|
||||
t.Errorf("%s: got:\n%s\nwant:\n%s\ndiff:\n%s\n", tt.desc, got, want, util.YAMLDiff(got, want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeNode(t *testing.T) {
|
||||
testTreeYAML := `
|
||||
a:
|
||||
b:
|
||||
c: val1
|
||||
list1:
|
||||
- i1: val1
|
||||
- i2: val2
|
||||
`
|
||||
tests := []struct {
|
||||
desc string
|
||||
baseYAML string
|
||||
path string
|
||||
value string
|
||||
want string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
desc: "merge list entry",
|
||||
baseYAML: testTreeYAML,
|
||||
path: "a.b.list1.[i1:val1]",
|
||||
value: `
|
||||
i2b: val2`,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
c: val1
|
||||
list1:
|
||||
- i1: val1
|
||||
i2b: val2
|
||||
- i2: val2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "merge list 2",
|
||||
baseYAML: testTreeYAML,
|
||||
path: "a.b.list1",
|
||||
value: `
|
||||
i3:
|
||||
a: val3
|
||||
`,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
c: val1
|
||||
list1:
|
||||
- i1: val1
|
||||
- i2: val2
|
||||
- i3:
|
||||
a: val3
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
root := make(map[string]any)
|
||||
if tt.baseYAML != "" {
|
||||
if err := yaml.Unmarshal([]byte(tt.baseYAML), &root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
p := util.PathFromString(tt.path)
|
||||
iv := make(map[string]any)
|
||||
err := yaml.Unmarshal([]byte(tt.value), &iv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = MergeNode(root, p, iv)
|
||||
if gotErr, wantErr := errToString(err), tt.wantErr; gotErr != wantErr {
|
||||
t.Errorf("%s: gotErr:%s, wantErr:%s", tt.desc, gotErr, wantErr)
|
||||
return
|
||||
}
|
||||
if got, want := util.ToYAML(root), tt.want; err == nil && util.YAMLDiff(got, want) != "" {
|
||||
t.Errorf("%s: got:\n%s\nwant:\n%s\ndiff:\n%s\n", tt.desc, got, want, util.YAMLDiff(got, want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// errToString returns the string representation of err and the empty string if
|
||||
// err is nil.
|
||||
func errToString(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
// TestSecretVolumes simulates https://github.com/istio/istio/issues/20381
|
||||
func TestSecretVolumes(t *testing.T) {
|
||||
rootYAML := `
|
||||
values:
|
||||
gateways:
|
||||
istio-egressgateway:
|
||||
secretVolumes: []
|
||||
`
|
||||
root := make(map[string]any)
|
||||
if err := yaml.Unmarshal([]byte(rootYAML), &root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
overrides := []struct {
|
||||
path string
|
||||
value any
|
||||
}{
|
||||
{
|
||||
path: "values.gateways.istio-egressgateway.secretVolumes[0].name",
|
||||
value: "egressgateway-certs",
|
||||
},
|
||||
{
|
||||
path: "values.gateways.istio-egressgateway.secretVolumes[0].secretName",
|
||||
value: "istio-egressgateway-certs",
|
||||
},
|
||||
{
|
||||
path: "values.gateways.istio-egressgateway.secretVolumes[0].mountPath",
|
||||
value: "/etc/istio/egressgateway-certs",
|
||||
},
|
||||
{
|
||||
path: "values.gateways.istio-egressgateway.secretVolumes[1].name",
|
||||
value: "egressgateway-ca-certs",
|
||||
},
|
||||
{
|
||||
path: "values.gateways.istio-egressgateway.secretVolumes[1].secretName",
|
||||
value: "istio-egressgateway-ca-certs",
|
||||
},
|
||||
{
|
||||
path: "values.gateways.istio-egressgateway.secretVolumes[1].mountPath",
|
||||
value: "/etc/istio/egressgateway-ca-certs",
|
||||
},
|
||||
{
|
||||
path: "values.gateways.istio-egressgateway.secretVolumes[2].name",
|
||||
value: "nginx-client-certs",
|
||||
},
|
||||
{
|
||||
path: "values.gateways.istio-egressgateway.secretVolumes[2].secretName",
|
||||
value: "nginx-client-certs",
|
||||
},
|
||||
{
|
||||
path: "values.gateways.istio-egressgateway.secretVolumes[2].mountPath",
|
||||
value: "/etc/istio/nginx-client-certs",
|
||||
},
|
||||
{
|
||||
path: "values.gateways.istio-egressgateway.secretVolumes[3].name",
|
||||
value: "nginx-ca-certs",
|
||||
},
|
||||
{
|
||||
path: "values.gateways.istio-egressgateway.secretVolumes[3].secretName",
|
||||
value: "nginx-ca-certs",
|
||||
},
|
||||
{
|
||||
path: "values.gateways.istio-egressgateway.secretVolumes[3].mountPath",
|
||||
value: "/etc/istio/nginx-ca-certs",
|
||||
},
|
||||
}
|
||||
|
||||
for _, override := range overrides {
|
||||
|
||||
pc, _, err := GetPathContext(root, util.PathFromString(override.path), true)
|
||||
if err != nil {
|
||||
t.Fatalf("GetPathContext(%q): %v", override.path, err)
|
||||
}
|
||||
err = WritePathContext(pc, override.value, false)
|
||||
if err != nil {
|
||||
t.Fatalf("WritePathContext(%q): %v", override.path, err)
|
||||
}
|
||||
}
|
||||
|
||||
want := `
|
||||
values:
|
||||
gateways:
|
||||
istio-egressgateway:
|
||||
secretVolumes:
|
||||
- mountPath: /etc/istio/egressgateway-certs
|
||||
name: egressgateway-certs
|
||||
secretName: istio-egressgateway-certs
|
||||
- mountPath: /etc/istio/egressgateway-ca-certs
|
||||
name: egressgateway-ca-certs
|
||||
secretName: istio-egressgateway-ca-certs
|
||||
- mountPath: /etc/istio/nginx-client-certs
|
||||
name: nginx-client-certs
|
||||
secretName: nginx-client-certs
|
||||
- mountPath: /etc/istio/nginx-ca-certs
|
||||
name: nginx-ca-certs
|
||||
secretName: nginx-ca-certs
|
||||
`
|
||||
gotYAML := util.ToYAML(root)
|
||||
diff := util.YAMLDiff(gotYAML, want)
|
||||
if diff != "" {
|
||||
t.Errorf("TestSecretVolumes: diff:\n%s\n", diff)
|
||||
}
|
||||
}
|
||||
|
||||
// Simulates https://github.com/istio/istio/issues/19196
|
||||
func TestWriteEscapedPathContext(t *testing.T) {
|
||||
rootYAML := `
|
||||
values:
|
||||
sidecarInjectorWebhook:
|
||||
injectedAnnotations: {}
|
||||
`
|
||||
tests := []struct {
|
||||
desc string
|
||||
path string
|
||||
value any
|
||||
want string
|
||||
wantFound bool
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
desc: "ModifyEscapedPathValue",
|
||||
path: `values.sidecarInjectorWebhook.injectedAnnotations.container\.apparmor\.security\.beta\.kubernetes\.io/istio-proxy`,
|
||||
value: `runtime/default`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
values:
|
||||
sidecarInjectorWebhook:
|
||||
injectedAnnotations:
|
||||
container.apparmor.security.beta.kubernetes.io/istio-proxy: runtime/default
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
root := make(map[string]any)
|
||||
if err := yaml.Unmarshal([]byte(rootYAML), &root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pc, gotFound, gotErr := GetPathContext(root, util.PathFromString(tt.path), false)
|
||||
if gotErr, wantErr := errToString(gotErr), tt.wantErr; gotErr != wantErr {
|
||||
t.Fatalf("GetPathContext(%s): gotErr:%s, wantErr:%s", tt.desc, gotErr, wantErr)
|
||||
}
|
||||
if gotFound != tt.wantFound {
|
||||
t.Fatalf("GetPathContext(%s): gotFound:%v, wantFound:%v", tt.desc, gotFound, tt.wantFound)
|
||||
}
|
||||
if tt.wantErr != "" || !tt.wantFound {
|
||||
return
|
||||
}
|
||||
|
||||
err := WritePathContext(pc, tt.value, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
gotYAML := util.ToYAML(root)
|
||||
diff := util.YAMLDiff(gotYAML, tt.want)
|
||||
if diff != "" {
|
||||
t.Errorf("%s: diff:\n%s\n", tt.desc, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
58
pkg/cmd/hgctl/helm/tpath/util.go
Normal file
58
pkg/cmd/hgctl/helm/tpath/util.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// 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 tpath
|
||||
|
||||
import (
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/util"
|
||||
"gopkg.in/yaml.v2"
|
||||
yaml2 "sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// AddSpecRoot adds a root node called "spec" to the given tree and returns the resulting tree.
|
||||
func AddSpecRoot(tree string) (string, error) {
|
||||
t, nt := make(map[string]any), make(map[string]any)
|
||||
if err := yaml.Unmarshal([]byte(tree), &t); err != nil {
|
||||
return "", err
|
||||
}
|
||||
nt["spec"] = t
|
||||
out, err := yaml.Marshal(nt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
// GetSpecSubtree returns the subtree under "spec".
|
||||
func GetSpecSubtree(yml string) (string, error) {
|
||||
return GetConfigSubtree(yml, "spec")
|
||||
}
|
||||
|
||||
// GetConfigSubtree returns the subtree at the given path.
|
||||
func GetConfigSubtree(manifest, path string) (string, error) {
|
||||
root := make(map[string]any)
|
||||
if err := yaml2.Unmarshal([]byte(manifest), &root); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
nc, _, err := GetPathContext(root, util.PathFromString(path), false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
out, err := yaml2.Marshal(nc.Node)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
122
pkg/cmd/hgctl/helm/tpath/util_test.go
Normal file
122
pkg/cmd/hgctl/helm/tpath/util_test.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tpath
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAddSpecRoot(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in string
|
||||
expect string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty",
|
||||
in: ``,
|
||||
expect: `spec: {}
|
||||
`,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "add-root",
|
||||
in: `
|
||||
a: va
|
||||
b: foo`,
|
||||
expect: `spec:
|
||||
a: va
|
||||
b: foo
|
||||
`,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "err",
|
||||
in: `i can't be yaml, can I?`,
|
||||
expect: ``,
|
||||
err: errors.New(""),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got, err := AddSpecRoot(tt.in); got != tt.expect ||
|
||||
((err != nil && tt.err == nil) || (err == nil && tt.err != nil)) {
|
||||
t.Errorf("%s AddSpecRoot(%s) => %s, want %s", tt.desc, tt.in, got, tt.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetConfigSubtree(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
manifest string
|
||||
path string
|
||||
expect string
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
desc: "empty",
|
||||
manifest: ``,
|
||||
path: ``,
|
||||
expect: `{}
|
||||
`,
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "subtree",
|
||||
manifest: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v2
|
||||
- list:
|
||||
- v1
|
||||
- v2
|
||||
- v3_regex
|
||||
name: n2
|
||||
`,
|
||||
path: `a`,
|
||||
expect: `b:
|
||||
- name: n1
|
||||
value: v2
|
||||
- list:
|
||||
- v1
|
||||
- v2
|
||||
- v3_regex
|
||||
name: n2
|
||||
`,
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "err",
|
||||
manifest: "not-yaml",
|
||||
path: "not-subnode",
|
||||
expect: ``,
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got, err := GetConfigSubtree(tt.manifest, tt.path); got != tt.expect || (err == nil) == tt.err {
|
||||
t.Errorf("%s GetConfigSubtree(%s, %s) => %s, want %s", tt.desc, tt.manifest, tt.path, got, tt.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
207
pkg/cmd/hgctl/install.go
Normal file
207
pkg/cmd/hgctl/install.go
Normal file
@@ -0,0 +1,207 @@
|
||||
// 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"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/installer"
|
||||
"github.com/alibaba/higress/pkg/cmd/options"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
setFlagHelpStr = `Override an higress profile value, e.g. to choose a profile
|
||||
(--set profile=local-k8s), or override profile values (--set gateway.replicas=2), or override helm values (--set values.global.proxy.resources.requsts.cpu=500m).`
|
||||
// manifestsFlagHelpStr is the command line description for --manifests
|
||||
manifestsFlagHelpStr = `Specify a path to a directory of profiles
|
||||
(e.g. ~/Downloads/higress/manifests).`
|
||||
filenameFlagHelpStr = "Path to file containing helm custom values"
|
||||
outputHelpstr = "Specify a file to write profile yaml"
|
||||
|
||||
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
|
||||
// Context is the cluster context in the kube config
|
||||
Context string
|
||||
// Set is a string with element format "path=value" where path is an profile path and the value is a
|
||||
// value to set the node at that path to.
|
||||
Set []string
|
||||
// ManifestsPath is a path to a ManifestsPath and profiles directory in the local filesystem with a release tgz.
|
||||
ManifestsPath string
|
||||
}
|
||||
|
||||
func (a *InstallArgs) String() string {
|
||||
var b strings.Builder
|
||||
b.WriteString("KubeConfigPath: " + a.KubeConfigPath + "\n")
|
||||
b.WriteString("Context: " + a.Context + "\n")
|
||||
b.WriteString("Set: " + fmt.Sprint(a.Set) + "\n")
|
||||
b.WriteString("ManifestsPath: " + a.ManifestsPath + "\n")
|
||||
return b.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)
|
||||
}
|
||||
|
||||
// --manifests is an alias for --set installPackagePath=
|
||||
func applyFlagAliases(flags []string, manifestsPath string) []string {
|
||||
if manifestsPath != "" {
|
||||
flags = append(flags, fmt.Sprintf("installPackagePath=%s", manifestsPath))
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
// newInstallCmd generates a higress install manifest and applies it to a cluster
|
||||
func newInstallCmd() *cobra.Command {
|
||||
iArgs := &InstallArgs{}
|
||||
installCmd := &cobra.Command{
|
||||
Use: "install",
|
||||
Short: "Applies an higress manifest, installing or reconfiguring higress on a cluster.",
|
||||
Long: "The install command generates an higress install manifest and applies it to a cluster.",
|
||||
// nolint: lll
|
||||
Example: ` # Apply a default higress installation
|
||||
hgctl install
|
||||
|
||||
# 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)
|
||||
},
|
||||
}
|
||||
addInstallFlags(installCmd, iArgs)
|
||||
flags := installCmd.Flags()
|
||||
options.AddKubeConfigFlags(flags)
|
||||
return installCmd
|
||||
}
|
||||
|
||||
func install(writer io.Writer, iArgs *InstallArgs) error {
|
||||
setFlags := applyFlagAliases(iArgs.Set, iArgs.ManifestsPath)
|
||||
|
||||
// check profileName
|
||||
psf := helm.GetValueForSetFlag(setFlags, "profile")
|
||||
if len(psf) == 0 {
|
||||
psf = promptProfileName(writer)
|
||||
setFlags = append(setFlags, fmt.Sprintf("profile=%s", psf))
|
||||
}
|
||||
|
||||
if !promptInstall(writer, psf) {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, profile, profileName, err := helm.GenerateConfig(iArgs.InFilenames, setFlags)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate config: %v", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(writer, "\n🧐 Validating Profile: \"%s\" \n", profileName)
|
||||
err = profile.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func promptInstall(writer io.Writer, profileName string) bool {
|
||||
answer := ""
|
||||
for {
|
||||
fmt.Fprintf(writer, "\nThis will install Higress \"%s\" profile into the cluster. \nProceed? (y/N)", profileName)
|
||||
fmt.Scanln(&answer)
|
||||
if strings.TrimSpace(answer) == "y" {
|
||||
fmt.Fprintf(writer, "\n")
|
||||
return true
|
||||
}
|
||||
if strings.TrimSpace(answer) == "N" {
|
||||
fmt.Fprintf(writer, "Cancelled.\n")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func promptProfileName(writer io.Writer) string {
|
||||
answer := ""
|
||||
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, "\nPlease input 1, 2 or 3 to select, input your selection:")
|
||||
fmt.Scanln(&answer)
|
||||
if strings.TrimSpace(answer) == "1" {
|
||||
return profileNameLocalK8s
|
||||
}
|
||||
if strings.TrimSpace(answer) == "2" {
|
||||
return profileNameK8s
|
||||
}
|
||||
if strings.TrimSpace(answer) == "3" {
|
||||
return profileNameLocalDocker
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func installManifests(profile *helm.Profile, writer io.Writer) error {
|
||||
installer, err := installer.NewInstaller(profile, writer, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = installer.Install()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
121
pkg/cmd/hgctl/installer/component.go
Normal file
121
pkg/cmd/hgctl/installer/component.go
Normal file
@@ -0,0 +1,121 @@
|
||||
// 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/helm"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/util"
|
||||
"helm.sh/helm/v3/pkg/chartutil"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
type ComponentName string
|
||||
|
||||
var ComponentMap = map[ComponentName]struct{}{
|
||||
Higress: {},
|
||||
Istio: {},
|
||||
}
|
||||
|
||||
type Component interface {
|
||||
// ComponentName returns the name of the component.
|
||||
ComponentName() ComponentName
|
||||
// Namespace returns the namespace for the component.
|
||||
Namespace() string
|
||||
// Enabled reports whether the component is enabled.
|
||||
Enabled() bool
|
||||
// Run starts the component. Must be called before the component is used.
|
||||
Run() error
|
||||
RenderManifest() (string, error)
|
||||
}
|
||||
|
||||
type ComponentOptions struct {
|
||||
Name string
|
||||
Namespace string
|
||||
// local
|
||||
ChartPath string
|
||||
// remote
|
||||
RepoURL string
|
||||
ChartName string
|
||||
Version string
|
||||
Quiet bool
|
||||
// Capabilities
|
||||
Capabilities *chartutil.Capabilities
|
||||
}
|
||||
|
||||
type ComponentOption func(*ComponentOptions)
|
||||
|
||||
func WithComponentNamespace(namespace string) ComponentOption {
|
||||
return func(opts *ComponentOptions) {
|
||||
opts.Namespace = namespace
|
||||
}
|
||||
}
|
||||
|
||||
func WithComponentChartPath(path string) ComponentOption {
|
||||
return func(opts *ComponentOptions) {
|
||||
opts.ChartPath = path
|
||||
}
|
||||
}
|
||||
|
||||
func WithComponentChartName(chartName string) ComponentOption {
|
||||
return func(opts *ComponentOptions) {
|
||||
opts.ChartName = chartName
|
||||
}
|
||||
}
|
||||
|
||||
func WithComponentRepoURL(url string) ComponentOption {
|
||||
return func(opts *ComponentOptions) {
|
||||
opts.RepoURL = url
|
||||
}
|
||||
}
|
||||
|
||||
func WithComponentVersion(version string) ComponentOption {
|
||||
return func(opts *ComponentOptions) {
|
||||
opts.Version = version
|
||||
}
|
||||
}
|
||||
|
||||
func WithComponentCapabilities(capabilities *chartutil.Capabilities) ComponentOption {
|
||||
return func(opts *ComponentOptions) {
|
||||
opts.Capabilities = capabilities
|
||||
}
|
||||
}
|
||||
|
||||
func WithQuiet() ComponentOption {
|
||||
return func(opts *ComponentOptions) {
|
||||
opts.Quiet = true
|
||||
}
|
||||
}
|
||||
|
||||
func renderComponentManifest(spec any, renderer helm.Renderer, addOn bool, name ComponentName, namespace string) (string, error) {
|
||||
var valsBytes []byte
|
||||
var valsYaml string
|
||||
var err error
|
||||
if yamlString, ok := spec.(string); ok {
|
||||
valsYaml = yamlString
|
||||
} else {
|
||||
if !util.IsValueNil(spec) {
|
||||
valsBytes, err = yaml.Marshal(spec)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
valsYaml = string(valsBytes)
|
||||
}
|
||||
}
|
||||
final, err := renderer.RenderManifest(valsYaml)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return final, nil
|
||||
}
|
||||
113
pkg/cmd/hgctl/installer/gateway_api.go
Normal file
113
pkg/cmd/hgctl/installer/gateway_api.go
Normal 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
|
||||
}
|
||||
93
pkg/cmd/hgctl/installer/helm_agent.go
Normal file
93
pkg/cmd/hgctl/installer/helm_agent.go
Normal 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
|
||||
}
|
||||
127
pkg/cmd/hgctl/installer/higress.go
Normal file
127
pkg/cmd/hgctl/installer/higress.go
Normal file
@@ -0,0 +1,127 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package installer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
|
||||
)
|
||||
|
||||
const (
|
||||
Higress ComponentName = "higress"
|
||||
)
|
||||
|
||||
type HigressComponent struct {
|
||||
profile *helm.Profile
|
||||
started bool
|
||||
opts *ComponentOptions
|
||||
renderer helm.Renderer
|
||||
writer io.Writer
|
||||
kubeCli kubernetes.CLIClient
|
||||
}
|
||||
|
||||
func (h *HigressComponent) ComponentName() ComponentName {
|
||||
return Higress
|
||||
}
|
||||
|
||||
func (h *HigressComponent) Namespace() string {
|
||||
return h.opts.Namespace
|
||||
}
|
||||
|
||||
func (h *HigressComponent) Enabled() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *HigressComponent) Run() error {
|
||||
// Parse latest version
|
||||
if h.opts.Version == helm.RepoLatestVersion {
|
||||
|
||||
latestVersion, err := helm.ParseLatestVersion(h.opts.RepoURL, h.opts.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !h.opts.Quiet {
|
||||
fmt.Fprintf(h.writer, "⚡️ Fetching Higress Helm Chart latest version \"%s\" \n", latestVersion)
|
||||
}
|
||||
|
||||
// Reset Helm Chart version
|
||||
h.opts.Version = latestVersion
|
||||
h.renderer.SetVersion(latestVersion)
|
||||
}
|
||||
if !h.opts.Quiet {
|
||||
fmt.Fprintf(h.writer, "🏄 Downloading Higress Helm Chart version: %s, url: %s\n", h.opts.Version, h.opts.RepoURL)
|
||||
}
|
||||
if err := h.renderer.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
h.profile.HigressVersion = h.opts.Version
|
||||
h.started = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HigressComponent) RenderManifest() (string, error) {
|
||||
if !h.started {
|
||||
return "", nil
|
||||
}
|
||||
if !h.opts.Quiet {
|
||||
fmt.Fprintf(h.writer, "📦 Rendering Higress Helm Chart\n")
|
||||
}
|
||||
valsYaml, err := h.profile.ValuesYaml()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
manifest, err2 := renderComponentManifest(valsYaml, h.renderer, true, h.ComponentName(), h.opts.Namespace)
|
||||
if err2 != nil {
|
||||
return "", err
|
||||
}
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
func NewHigressComponent(kubeCli kubernetes.CLIClient, profile *helm.Profile, writer io.Writer, opts ...ComponentOption) (Component, error) {
|
||||
newOpts := &ComponentOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(newOpts)
|
||||
}
|
||||
|
||||
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{
|
||||
profile: profile,
|
||||
renderer: renderer,
|
||||
opts: newOpts,
|
||||
writer: writer,
|
||||
kubeCli: kubeCli,
|
||||
}
|
||||
return higressComponent, nil
|
||||
}
|
||||
130
pkg/cmd/hgctl/installer/installer.go
Normal file
130
pkg/cmd/hgctl/installer/installer.go
Normal 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 installer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
|
||||
"github.com/alibaba/higress/pkg/cmd/options"
|
||||
"k8s.io/client-go/util/homedir"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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("failed to build kubernetes client: %w", err)
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
func GetHomeDir() (string, error) {
|
||||
home := homedir.HomeDir()
|
||||
if home == "" {
|
||||
return "", fmt.Errorf("No user home environment variable found for OS %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
return home, nil
|
||||
}
|
||||
|
||||
func GetHgctlPath() (string, error) {
|
||||
home, err := GetHomeDir()
|
||||
if 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 hgctlPath, nil
|
||||
}
|
||||
|
||||
func GetDefaultInstallPackagePath() (string, error) {
|
||||
dir, err := os.Getwd()
|
||||
if 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 path, err
|
||||
}
|
||||
|
||||
func GetProfileInstalledPath() (string, error) {
|
||||
hgctlPath, err := GetHgctlPath()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
profilesPath := filepath.Join(hgctlPath, ProfileInstalledPath)
|
||||
if _, err := os.Stat(profilesPath); os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(profilesPath, os.ModePerm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
112
pkg/cmd/hgctl/installer/installer_docker.go
Normal file
112
pkg/cmd/hgctl/installer/installer_docker.go
Normal 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
|
||||
}
|
||||
343
pkg/cmd/hgctl/installer/installer_k8s.go
Normal file
343
pkg/cmd/hgctl/installer/installer_k8s.go
Normal 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
|
||||
}
|
||||
125
pkg/cmd/hgctl/installer/istio.go
Normal file
125
pkg/cmd/hgctl/installer/istio.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// 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 (
|
||||
"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 (
|
||||
Istio ComponentName = "istio"
|
||||
)
|
||||
|
||||
type IstioCRDComponent struct {
|
||||
profile *helm.Profile
|
||||
started bool
|
||||
opts *ComponentOptions
|
||||
renderer helm.Renderer
|
||||
writer io.Writer
|
||||
kubeCli kubernetes.CLIClient
|
||||
}
|
||||
|
||||
func NewIstioCRDComponent(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
|
||||
|
||||
// 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.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
|
||||
}
|
||||
}
|
||||
|
||||
istioComponent := &IstioCRDComponent{
|
||||
profile: profile,
|
||||
renderer: renderer,
|
||||
opts: newOpts,
|
||||
writer: writer,
|
||||
kubeCli: kubeCli,
|
||||
}
|
||||
return istioComponent, nil
|
||||
}
|
||||
|
||||
func (i *IstioCRDComponent) ComponentName() ComponentName {
|
||||
return Istio
|
||||
}
|
||||
|
||||
func (i *IstioCRDComponent) Namespace() string {
|
||||
return i.opts.Namespace
|
||||
}
|
||||
|
||||
func (i *IstioCRDComponent) Enabled() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (i *IstioCRDComponent) Run() error {
|
||||
if !i.opts.Quiet {
|
||||
fmt.Fprintf(i.writer, "🏄 Downloading Istio Helm Chart 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 *IstioCRDComponent) RenderManifest() (string, error) {
|
||||
if !i.started {
|
||||
return "", nil
|
||||
}
|
||||
if !i.opts.Quiet {
|
||||
fmt.Fprintf(i.writer, "📦 Rendering Istio Helm Chart\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
|
||||
}
|
||||
247
pkg/cmd/hgctl/installer/profile_store.go
Normal file
247
pkg/cmd/hgctl/installer/profile_store.go
Normal 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
|
||||
}
|
||||
66
pkg/cmd/hgctl/installer/server_info.go
Normal file
66
pkg/cmd/hgctl/installer/server_info.go
Normal 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
|
||||
}
|
||||
141
pkg/cmd/hgctl/installer/standalone.go
Normal file
141
pkg/cmd/hgctl/installer/standalone.go
Normal 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
|
||||
}
|
||||
356
pkg/cmd/hgctl/installer/standalone_agent.go
Normal file
356
pkg/cmd/hgctl/installer/standalone_agent.go
Normal 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
|
||||
}
|
||||
@@ -19,17 +19,21 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
kubescheme "k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
"k8s.io/client-go/util/retry"
|
||||
ctrClient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
type CLIClient interface {
|
||||
@@ -44,6 +48,18 @@ type CLIClient interface {
|
||||
|
||||
// PodExec takes a command and the pod data to run the command in the specified pod.
|
||||
PodExec(namespacedName types.NamespacedName, container string, command string) (stdout string, stderr string, err error)
|
||||
|
||||
// ApplyObject creates or updates unstructured object
|
||||
ApplyObject(obj *unstructured.Unstructured) error
|
||||
|
||||
// DeleteObject delete unstructured object
|
||||
DeleteObject(obj *unstructured.Unstructured) error
|
||||
|
||||
// CreateNamespace create namespace
|
||||
CreateNamespace(namespace string) error
|
||||
|
||||
// KubernetesInterface get kubernetes interface
|
||||
KubernetesInterface() kubernetes.Interface
|
||||
}
|
||||
|
||||
var _ CLIClient = &client{}
|
||||
@@ -52,6 +68,7 @@ type client struct {
|
||||
config *rest.Config
|
||||
restClient *rest.RESTClient
|
||||
kube kubernetes.Interface
|
||||
ctrClient ctrClient.Client
|
||||
}
|
||||
|
||||
func NewCLIClient(clientConfig clientcmd.ClientConfig) (CLIClient, error) {
|
||||
@@ -80,33 +97,13 @@ func newClientInternal(clientConfig clientcmd.ClientConfig) (*client, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.ctrClient, err = ctrClient.New(c.config, ctrClient.Options{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, err
|
||||
}
|
||||
|
||||
func setRestDefaults(config *rest.Config) *rest.Config {
|
||||
if config.GroupVersion == nil || config.GroupVersion.Empty() {
|
||||
config.GroupVersion = &corev1.SchemeGroupVersion
|
||||
}
|
||||
if len(config.APIPath) == 0 {
|
||||
if len(config.GroupVersion.Group) == 0 {
|
||||
config.APIPath = "/api"
|
||||
} else {
|
||||
config.APIPath = "/apis"
|
||||
}
|
||||
}
|
||||
if len(config.ContentType) == 0 {
|
||||
config.ContentType = runtime.ContentTypeJSON
|
||||
}
|
||||
if config.NegotiatedSerializer == nil {
|
||||
// This codec factory ensures the resources are not converted. Therefore, resources
|
||||
// will not be round-tripped through internal versions. Defaulting does not happen
|
||||
// on the client.
|
||||
config.NegotiatedSerializer = serializer.NewCodecFactory(kubescheme.Scheme).WithoutConversion()
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func (c *client) RESTConfig() *rest.Config {
|
||||
if c.config == nil {
|
||||
return nil
|
||||
@@ -170,3 +167,91 @@ func (c *client) PodExec(namespacedName types.NamespacedName, container string,
|
||||
stderr = stderrBuf.String()
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteObject delete unstructured object
|
||||
func (c *client) DeleteObject(obj *unstructured.Unstructured) error {
|
||||
err := c.ctrClient.Delete(context.TODO(), obj, ctrClient.PropagationPolicy(metav1.DeletePropagationBackground))
|
||||
if err != nil {
|
||||
if !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyObject creates or updates unstructured object
|
||||
func (c *client) ApplyObject(obj *unstructured.Unstructured) error {
|
||||
if obj.GetKind() == "List" {
|
||||
objList, err := obj.ToList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range objList.Items {
|
||||
if err := c.ApplyObject(&item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
key := ctrClient.ObjectKeyFromObject(obj)
|
||||
receiver := &unstructured.Unstructured{}
|
||||
receiver.SetGroupVersionKind(obj.GroupVersionKind())
|
||||
|
||||
if err := retry.RetryOnConflict(wait.Backoff{
|
||||
Duration: time.Millisecond * 10,
|
||||
Factor: 2,
|
||||
Steps: 3,
|
||||
}, func() error {
|
||||
if err := c.ctrClient.Get(context.Background(), key, receiver); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
if err := c.ctrClient.Create(context.Background(), obj); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := applyOverlay(receiver, obj); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.ctrClient.Update(context.Background(), receiver); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateNamespace create namespace
|
||||
func (c *client) CreateNamespace(namespace string) error {
|
||||
key := ctrClient.ObjectKey{
|
||||
Namespace: metav1.NamespaceSystem,
|
||||
Name: namespace,
|
||||
}
|
||||
if err := c.ctrClient.Get(context.Background(), key, &corev1.Namespace{}); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
nsObj := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: metav1.NamespaceSystem,
|
||||
Name: namespace,
|
||||
},
|
||||
}
|
||||
if err := c.ctrClient.Create(context.Background(), nsObj); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to check if namespace %v exists: %v", namespace, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// KubernetesInterface get kubernetes interface
|
||||
func (c *client) KubernetesInterface() kubernetes.Interface {
|
||||
return c.kube
|
||||
|
||||
}
|
||||
|
||||
157
pkg/cmd/hgctl/kubernetes/common.go
Normal file
157
pkg/cmd/hgctl/kubernetes/common.go
Normal file
@@ -0,0 +1,157 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch/v5"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
kubescheme "k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
)
|
||||
|
||||
// applyOverlay applies an overlay using JSON patch strategy over the current Object in place.
|
||||
func applyOverlay(current, overlay *unstructured.Unstructured) error {
|
||||
cj, err := runtime.Encode(unstructured.UnstructuredJSONScheme, current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
overlayUpdated := overlay.DeepCopy()
|
||||
if strings.EqualFold(current.GetKind(), "service") {
|
||||
if err := saveClusterIP(current, overlayUpdated); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
saveNodePorts(current, overlayUpdated)
|
||||
}
|
||||
|
||||
if current.GetKind() == "PersistentVolumeClaim" {
|
||||
if err := savePersistentVolumeClaim(current, overlayUpdated); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
uj, err := runtime.Encode(unstructured.UnstructuredJSONScheme, overlayUpdated)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
merged, err := jsonpatch.MergePatch(cj, uj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runtime.DecodeInto(unstructured.UnstructuredJSONScheme, merged, current)
|
||||
}
|
||||
|
||||
// createPortMap returns a map, mapping the value of the port and value of the nodePort
|
||||
func createPortMap(current *unstructured.Unstructured) map[string]uint32 {
|
||||
portMap := make(map[string]uint32)
|
||||
svc := &corev1.Service{}
|
||||
if err := scheme.Scheme.Convert(current, svc, nil); err != nil {
|
||||
return portMap
|
||||
}
|
||||
for _, p := range svc.Spec.Ports {
|
||||
portMap[strconv.Itoa(int(p.Port))] = uint32(p.NodePort)
|
||||
}
|
||||
return portMap
|
||||
}
|
||||
|
||||
// savePersistentVolumeClaim copies the storageClassName from the current cluster into the overlay
|
||||
func savePersistentVolumeClaim(current, overlay *unstructured.Unstructured) error {
|
||||
// Save the value of spec.storageClassName set by the cluster
|
||||
if storageClassName, found, err := unstructured.NestedString(current.Object, "spec",
|
||||
"storageClassName"); err != nil {
|
||||
return err
|
||||
} else if found {
|
||||
if _, _, err2 := unstructured.NestedString(overlay.Object, "spec",
|
||||
"storageClassName"); err2 != nil {
|
||||
// override when overlay storageClassName property is not existed
|
||||
if err3 := unstructured.SetNestedField(overlay.Object, storageClassName, "spec",
|
||||
"storageClassName"); err3 != nil {
|
||||
return err3
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// saveNodePorts transfers the port values from the current cluster into the overlay
|
||||
func saveNodePorts(current, overlay *unstructured.Unstructured) {
|
||||
portMap := createPortMap(current)
|
||||
ports, _, _ := unstructured.NestedFieldNoCopy(overlay.Object, "spec", "ports")
|
||||
portList, ok := ports.([]any)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for _, port := range portList {
|
||||
m, ok := port.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if nodePortNum, ok := m["nodePort"]; ok && fmt.Sprintf("%v", nodePortNum) == "0" {
|
||||
if portNum, ok := m["port"]; ok {
|
||||
if v, ok := portMap[fmt.Sprintf("%v", portNum)]; ok {
|
||||
m["nodePort"] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// saveClusterIP copies the cluster IP from the current cluster into the overlay
|
||||
func saveClusterIP(current, overlay *unstructured.Unstructured) error {
|
||||
// Save the value of spec.clusterIP set by the cluster
|
||||
if clusterIP, found, err := unstructured.NestedString(current.Object, "spec",
|
||||
"clusterIP"); err != nil {
|
||||
return err
|
||||
} else if found {
|
||||
if err := unstructured.SetNestedField(overlay.Object, clusterIP, "spec",
|
||||
"clusterIP"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setRestDefaults(config *rest.Config) *rest.Config {
|
||||
if config.GroupVersion == nil || config.GroupVersion.Empty() {
|
||||
config.GroupVersion = &corev1.SchemeGroupVersion
|
||||
}
|
||||
if len(config.APIPath) == 0 {
|
||||
if len(config.GroupVersion.Group) == 0 {
|
||||
config.APIPath = "/api"
|
||||
} else {
|
||||
config.APIPath = "/apis"
|
||||
}
|
||||
}
|
||||
if len(config.ContentType) == 0 {
|
||||
config.ContentType = runtime.ContentTypeJSON
|
||||
}
|
||||
if config.NegotiatedSerializer == nil {
|
||||
// This codec factory ensures the resources are not converted. Therefore, resources
|
||||
// will not be round-tripped through internal versions. Defaulting does not happen
|
||||
// on the client.
|
||||
config.NegotiatedSerializer = serializer.NewCodecFactory(kubescheme.Scheme).WithoutConversion()
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -48,6 +44,9 @@ type PortForwarder interface {
|
||||
|
||||
// Address returns the address of the local forwarded address.
|
||||
Address() string
|
||||
|
||||
// WaitForStop blocks until connection closed (e.g. control-C interrupt)
|
||||
WaitForStop()
|
||||
}
|
||||
|
||||
var _ PortForwarder = &localForwarder{}
|
||||
@@ -56,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")
|
||||
}
|
||||
@@ -133,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,
|
||||
@@ -151,5 +152,9 @@ 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() {
|
||||
<-f.stopCh
|
||||
}
|
||||
|
||||
130
pkg/cmd/hgctl/kubernetes/wasmplugin.go
Normal file
130
pkg/cmd/hgctl/kubernetes/wasmplugin.go
Normal 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{})
|
||||
}
|
||||
146
pkg/cmd/hgctl/manifest.go
Normal file
146
pkg/cmd/hgctl/manifest.go
Normal file
@@ -0,0 +1,146 @@
|
||||
// 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"
|
||||
"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"
|
||||
)
|
||||
|
||||
type ManifestArgs struct {
|
||||
InFilenames []string
|
||||
// KubeConfigPath is the path to kube config file.
|
||||
KubeConfigPath string
|
||||
// Context is the cluster context in the kube config
|
||||
Context string
|
||||
// Set is a string with element format "path=value" where path is an profile path and the value is a
|
||||
// value to set the node at that path to.
|
||||
Set []string
|
||||
// ManifestsPath is a path to a ManifestsPath and profiles directory in the local filesystem with a release tgz.
|
||||
ManifestsPath string
|
||||
}
|
||||
|
||||
func (a *ManifestArgs) String() string {
|
||||
var b strings.Builder
|
||||
b.WriteString("KubeConfigPath: " + a.KubeConfigPath + "\n")
|
||||
b.WriteString("Context: " + a.Context + "\n")
|
||||
b.WriteString("Set: " + fmt.Sprint(a.Set) + "\n")
|
||||
b.WriteString("ManifestsPath: " + a.ManifestsPath + "\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// newManifestCmd generates a higress install manifest and applies it to a cluster
|
||||
func newManifestCmd() *cobra.Command {
|
||||
iArgs := &ManifestArgs{}
|
||||
manifestCmd := &cobra.Command{
|
||||
Use: "manifest",
|
||||
Short: "Generate higress manifests.",
|
||||
Long: "The manifest command generates an higress install manifests.",
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// newManifestGenerateCmd generates a higress install manifest and applies it to a cluster
|
||||
func newManifestGenerateCmd(iArgs *ManifestArgs) *cobra.Command {
|
||||
installCmd := &cobra.Command{
|
||||
Use: "generate",
|
||||
Short: "Generate higress manifests.",
|
||||
Long: "The manifest generate command generates higress install manifests.",
|
||||
// nolint: lll
|
||||
Example: ` # Generate higress manifests
|
||||
hgctl manifest generate
|
||||
`,
|
||||
Args: cobra.ExactArgs(0),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return generate(cmd.OutOrStdout(), iArgs)
|
||||
},
|
||||
}
|
||||
|
||||
return installCmd
|
||||
}
|
||||
|
||||
func generate(writer io.Writer, iArgs *ManifestArgs) error {
|
||||
setFlags := applyFlagAliases(iArgs.Set, iArgs.ManifestsPath)
|
||||
|
||||
// check profileName
|
||||
psf := helm.GetValueForSetFlag(setFlags, "profile")
|
||||
if len(psf) == 0 {
|
||||
setFlags = append(setFlags, fmt.Sprintf("profile=%s", helm.InstallLocalK8s))
|
||||
}
|
||||
|
||||
_, profile, _, err := helm.GenerateConfig(iArgs.InFilenames, setFlags)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate config: %v", err)
|
||||
}
|
||||
|
||||
err = profile.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = genManifests(profile, writer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to install manifests: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func genManifests(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.NewK8sInstaller(profile, cliClient, writer, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := op.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifestMap, err := op.RenderManifests()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := op.GenerateManifests(manifestMap); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
11763
pkg/cmd/hgctl/manifests/gatewayapi/experimental-install.yaml
Normal file
11763
pkg/cmd/hgctl/manifests/gatewayapi/experimental-install.yaml
Normal file
File diff suppressed because it is too large
Load Diff
10
pkg/cmd/hgctl/manifests/istiobase/Chart.yaml
Normal file
10
pkg/cmd/hgctl/manifests/istiobase/Chart.yaml
Normal 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
|
||||
21
pkg/cmd/hgctl/manifests/istiobase/README.md
Normal file
21
pkg/cmd/hgctl/manifests/istiobase/README.md
Normal 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
|
||||
```
|
||||
7199
pkg/cmd/hgctl/manifests/istiobase/crds/crd-all.gen.yaml
Normal file
7199
pkg/cmd/hgctl/manifests/istiobase/crds/crd-all.gen.yaml
Normal file
File diff suppressed because it is too large
Load Diff
48
pkg/cmd/hgctl/manifests/istiobase/crds/crd-operator.yaml
Normal file
48
pkg/cmd/hgctl/manifests/istiobase/crds/crd-operator.yaml
Normal 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
|
||||
---
|
||||
5
pkg/cmd/hgctl/manifests/istiobase/templates/NOTES.txt
Normal file
5
pkg/cmd/hgctl/manifests/istiobase/templates/NOTES.txt
Normal 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 }}
|
||||
181
pkg/cmd/hgctl/manifests/istiobase/templates/clusterrole.yaml
Normal file
181
pkg/cmd/hgctl/manifests/istiobase/templates/clusterrole.yaml
Normal 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}}
|
||||
---
|
||||
@@ -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 }}
|
||||
---
|
||||
4
pkg/cmd/hgctl/manifests/istiobase/templates/crds.yaml
Normal file
4
pkg/cmd/hgctl/manifests/istiobase/templates/crds.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
{{- if .Values.base.enableCRDTemplates }}
|
||||
{{ .Files.Get "crds/crd-all.gen.yaml" }}
|
||||
{{ .Files.Get "crds/crd-operator.yaml" }}
|
||||
{{- end }}
|
||||
48
pkg/cmd/hgctl/manifests/istiobase/templates/default.yaml
Normal file
48
pkg/cmd/hgctl/manifests/istiobase/templates/default.yaml
Normal 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 }}
|
||||
23
pkg/cmd/hgctl/manifests/istiobase/templates/endpoints.yaml
Normal file
23
pkg/cmd/hgctl/manifests/istiobase/templates/endpoints.yaml
Normal 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 }}
|
||||
@@ -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 }}
|
||||
25
pkg/cmd/hgctl/manifests/istiobase/templates/role.yaml
Normal file
25
pkg/cmd/hgctl/manifests/istiobase/templates/role.yaml
Normal 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"]
|
||||
21
pkg/cmd/hgctl/manifests/istiobase/templates/rolebinding.yaml
Normal file
21
pkg/cmd/hgctl/manifests/istiobase/templates/rolebinding.yaml
Normal 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 }}
|
||||
@@ -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 }}
|
||||
28
pkg/cmd/hgctl/manifests/istiobase/templates/services.yaml
Normal file
28
pkg/cmd/hgctl/manifests/istiobase/templates/services.yaml
Normal 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 }}
|
||||
29
pkg/cmd/hgctl/manifests/istiobase/values.yaml
Normal file
29
pkg/cmd/hgctl/manifests/istiobase/values.yaml
Normal 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"
|
||||
37
pkg/cmd/hgctl/manifests/manifest.go
Normal file
37
pkg/cmd/hgctl/manifests/manifest.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// 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 manifests
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"os"
|
||||
)
|
||||
|
||||
// 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
|
||||
// FS will be used
|
||||
func BuiltinOrDir(dir string) fs.FS {
|
||||
if dir == "" {
|
||||
return FS
|
||||
}
|
||||
return os.DirFS(dir)
|
||||
}
|
||||
42
pkg/cmd/hgctl/manifests/profiles/_all.yaml
Normal file
42
pkg/cmd/hgctl/manifests/profiles/_all.yaml
Normal file
@@ -0,0 +1,42 @@
|
||||
profile: all
|
||||
global:
|
||||
install: k8s # install mode k8s/local-k8s/local-docker/local
|
||||
ingressClass: higress
|
||||
enableIstioAPI: true
|
||||
enableGatewayAPI: false
|
||||
namespace: higress-system
|
||||
|
||||
console:
|
||||
port: 8080
|
||||
replicas: 1
|
||||
o11yEnabled: false
|
||||
|
||||
gateway:
|
||||
replicas: 1
|
||||
httpPort: 80
|
||||
httpsPort: 443
|
||||
metricsPort: 15020
|
||||
|
||||
controller:
|
||||
replicas: 1
|
||||
|
||||
storage:
|
||||
url: nacos://127.0.0.1:8848 # file://opt/higress/conf
|
||||
ns: higress-system
|
||||
username:
|
||||
password:
|
||||
dataEncKey:
|
||||
|
||||
# values passed through to helm
|
||||
values:
|
||||
|
||||
|
||||
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
|
||||
30
pkg/cmd/hgctl/manifests/profiles/k8s.yaml
Normal file
30
pkg/cmd/hgctl/manifests/profiles/k8s.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
profile: k8s
|
||||
global:
|
||||
install: k8s # install mode k8s/local-k8s/local-docker/local
|
||||
ingressClass: higress
|
||||
enableIstioAPI: false
|
||||
enableGatewayAPI: false
|
||||
namespace: higress-system
|
||||
|
||||
console:
|
||||
replicas: 1
|
||||
o11yEnabled: false
|
||||
|
||||
gateway:
|
||||
replicas: 2
|
||||
|
||||
controller:
|
||||
replicas: 1
|
||||
|
||||
# values passed through to helm
|
||||
values:
|
||||
|
||||
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
|
||||
31
pkg/cmd/hgctl/manifests/profiles/local-docker.yaml
Normal file
31
pkg/cmd/hgctl/manifests/profiles/local-docker.yaml
Normal 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
|
||||
|
||||
30
pkg/cmd/hgctl/manifests/profiles/local-k8s.yaml
Normal file
30
pkg/cmd/hgctl/manifests/profiles/local-k8s.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
profile: local-k8s
|
||||
global:
|
||||
install: local-k8s # install mode k8s/local-k8s/local-docker/local
|
||||
ingressClass: higress
|
||||
enableIstioAPI: true
|
||||
enableGatewayAPI: true
|
||||
namespace: higress-system
|
||||
|
||||
console:
|
||||
replicas: 1
|
||||
o11yEnabled: true
|
||||
|
||||
gateway:
|
||||
replicas: 1
|
||||
|
||||
controller:
|
||||
replicas: 1
|
||||
|
||||
# values passed through to helm
|
||||
values:
|
||||
|
||||
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
|
||||
778
pkg/cmd/hgctl/plugin/build/build.go
Normal file
778
pkg/cmd/hgctl/plugin/build/build.go
Normal 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)
|
||||
}
|
||||
194
pkg/cmd/hgctl/plugin/build/templates.go
Normal file
194
pkg/cmd/hgctl/plugin/build/templates.go
Normal 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
|
||||
}
|
||||
30
pkg/cmd/hgctl/plugin/config/config.go
Normal file
30
pkg/cmd/hgctl/plugin/config/config.go
Normal 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
|
||||
}
|
||||
76
pkg/cmd/hgctl/plugin/config/create.go
Normal file
76
pkg/cmd/hgctl/plugin/config/create.go
Normal 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",
|
||||
}
|
||||
143
pkg/cmd/hgctl/plugin/config/edit.go
Normal file
143
pkg/cmd/hgctl/plugin/config/edit.go
Normal 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
|
||||
}
|
||||
160
pkg/cmd/hgctl/plugin/config/templates.go
Normal file
160
pkg/cmd/hgctl/plugin/config/templates.go
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
92
pkg/cmd/hgctl/plugin/init/init.go
Normal file
92
pkg/cmd/hgctl/plugin/init/init.go
Normal 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
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user