mirror of
https://github.com/alibaba/higress.git
synced 2026-03-18 01:07:29 +08:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
20
.github/workflows/build-and-test.yaml
vendored
20
.github/workflows/build-and-test.yaml
vendored
@@ -38,10 +38,9 @@ jobs:
|
|||||||
path: |-
|
path: |-
|
||||||
envoy
|
envoy
|
||||||
istio
|
istio
|
||||||
external
|
|
||||||
.git/modules
|
.git/modules
|
||||||
key: ${{ runner.os }}-submodules-${{ github.run_id }}
|
key: ${{ runner.os }}-submodules-new-${{ github.run_id }}
|
||||||
restore-keys: ${{ runner.os }}-submodules
|
restore-keys: ${{ runner.os }}-submodules-new
|
||||||
|
|
||||||
- run: git stash # restore patch
|
- run: git stash # restore patch
|
||||||
|
|
||||||
@@ -85,10 +84,9 @@ jobs:
|
|||||||
path: |-
|
path: |-
|
||||||
envoy
|
envoy
|
||||||
istio
|
istio
|
||||||
external
|
|
||||||
.git/modules
|
.git/modules
|
||||||
key: ${{ runner.os }}-submodules-${{ github.run_id }}
|
key: ${{ runner.os }}-submodules-new-${{ github.run_id }}
|
||||||
restore-keys: ${{ runner.os }}-submodules
|
restore-keys: ${{ runner.os }}-submodules-new
|
||||||
|
|
||||||
- run: git stash # restore patch
|
- run: git stash # restore patch
|
||||||
|
|
||||||
@@ -134,10 +132,9 @@ jobs:
|
|||||||
path: |-
|
path: |-
|
||||||
envoy
|
envoy
|
||||||
istio
|
istio
|
||||||
external
|
|
||||||
.git/modules
|
.git/modules
|
||||||
key: ${{ runner.os }}-submodules-${{ github.run_id }}
|
key: ${{ runner.os }}-submodules-new-${{ github.run_id }}
|
||||||
restore-keys: ${{ runner.os }}-submodules
|
restore-keys: ${{ runner.os }}-submodules-new
|
||||||
|
|
||||||
- run: git stash # restore patch
|
- run: git stash # restore patch
|
||||||
|
|
||||||
@@ -175,10 +172,9 @@ jobs:
|
|||||||
path: |-
|
path: |-
|
||||||
envoy
|
envoy
|
||||||
istio
|
istio
|
||||||
external
|
|
||||||
.git/modules
|
.git/modules
|
||||||
key: ${{ runner.os }}-submodules-${{ github.run_id }}
|
key: ${{ runner.os }}-submodules-new-${{ github.run_id }}
|
||||||
restore-keys: ${{ runner.os }}-submodules
|
restore-keys: ${{ runner.os }}-submodules-new
|
||||||
|
|
||||||
- run: git stash # restore patch
|
- run: git stash # restore patch
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/build-image-and-push.yaml
vendored
2
.github/workflows/build-image-and-push.yaml
vendored
@@ -41,7 +41,7 @@ jobs:
|
|||||||
envoy
|
envoy
|
||||||
istio
|
istio
|
||||||
.git/modules
|
.git/modules
|
||||||
key: ${{ runner.os }}-submodules-${{ github.run_id }}
|
key: ${{ runner.os }}-submodules-new-${{ github.run_id }}
|
||||||
restore-keys: ${{ runner.os }}-submodules-new
|
restore-keys: ${{ runner.os }}-submodules-new
|
||||||
|
|
||||||
- name: Calculate Docker metadata
|
- name: Calculate Docker metadata
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ header:
|
|||||||
- 'tools/'
|
- 'tools/'
|
||||||
- 'test/README.md'
|
- 'test/README.md'
|
||||||
- 'pkg/cmd/hgctl/testdata/config'
|
- 'pkg/cmd/hgctl/testdata/config'
|
||||||
|
- 'pkg/cmd/hgctl/manifests'
|
||||||
|
|
||||||
comment: on-failure
|
comment: on-failure
|
||||||
dependency:
|
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)
|
GOPROXY=$(GOPROXY) GOOS=linux GOARCH=arm64 LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh ./out/linux_arm64/ $(HIGRESS_BINARIES)
|
||||||
|
|
||||||
.PHONY: build-hgctl
|
.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)
|
GOPROXY=$(GOPROXY) GOOS=$(GOOS_LOCAL) GOARCH=$(GOARCH_LOCAL) LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh $(OUT)/ $(HGCTL_BINARIES)
|
||||||
|
|
||||||
.PHONY: build-linux-hgctl
|
.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)
|
GOPROXY=$(GOPROXY) GOOS=linux GOARCH=$(GOARCH_LOCAL) LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh $(OUT_LINUX)/ $(HGCTL_BINARIES)
|
||||||
|
|
||||||
.PHONY: build-hgctl-multiarch
|
.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=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=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)
|
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:
|
external/package/envoy-amd64.tar.gz:
|
||||||
# cd external/proxy; BUILD_WITH_CONTAINER=1 make test_release
|
# 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.2.0/envoy-amd64.tar.gz"
|
||||||
|
|
||||||
external/package/envoy-arm64.tar.gz:
|
external/package/envoy-arm64.tar.gz:
|
||||||
# cd external/proxy; BUILD_WITH_CONTAINER=1 make test_release
|
# 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.2.0/envoy-arm64.tar.gz"
|
||||||
|
|
||||||
|
|
||||||
build-pilot:
|
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_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
|
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
|
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
|
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
|
build-gateway-local: prebuild external/package/envoy-amd64.tar.gz external/package/envoy-arm64.tar.gz build-pilot
|
||||||
cd external/istio; BUILD_WITH_CONTAINER=1 BUILDX_PLATFORM=true DOCKER_BUILD_VARIANTS=default DOCKER_TARGETS="docker.pilot" make docker
|
cd external/istio; rm -rf out/linux_${GOARCH_LOCAL}; GOOS_LOCAL=linux TARGET_OS=linux BUILD_WITH_CONTAINER=1 BUILDX_PLATFORM=false DOCKER_BUILD_VARIANTS=default DOCKER_TARGETS="docker.proxyv2" make docker
|
||||||
|
|
||||||
build-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
|
./tools/hack/build-wasm-plugins.sh
|
||||||
|
|
||||||
pre-install:
|
pre-install:
|
||||||
@@ -168,8 +176,8 @@ install: pre-install
|
|||||||
cd helm/higress; helm dependency build
|
cd helm/higress; helm dependency build
|
||||||
helm install higress helm/higress -n higress-system --create-namespace --set 'global.local=true'
|
helm install higress helm/higress -n higress-system --create-namespace --set 'global.local=true'
|
||||||
|
|
||||||
ENVOY_LATEST_IMAGE_TAG ?= 1.1.1
|
ENVOY_LATEST_IMAGE_TAG ?= sha-6835486
|
||||||
ISTIO_LATEST_IMAGE_TAG ?= 1.1.1
|
ISTIO_LATEST_IMAGE_TAG ?= sha-6835486
|
||||||
|
|
||||||
install-dev: pre-install
|
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'
|
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
|
.PHONY: kube-load-image
|
||||||
kube-load-image: $(tools/kind) ## Install the Higress image to a kind cluster using the provided $IMAGE and $TAG.
|
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/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 docker.io/alihigress/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 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/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/charlie1380/eureka-registry-provider v0.3.0
|
||||||
tools/hack/docker-pull-image.sh docker.io/bitinit/eureka latest
|
tools/hack/docker-pull-image.sh docker.io/bitinit/eureka latest
|
||||||
tools/hack/docker-pull-image.sh registry.cn-hangzhou.aliyuncs.com/2456868764/httpbin 1.0.2
|
tools/hack/docker-pull-image.sh docker.io/alihigress/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 docker.io/alihigress/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/kind-load-image.sh docker.io/alihigress/nacos-standlone-rc3 1.0.0-RC3
|
||||||
tools/hack/kind-load-image.sh docker.io/hashicorp/consul 1.16.0
|
tools/hack/kind-load-image.sh docker.io/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/charlie1380/eureka-registry-provider v0.3.0
|
||||||
tools/hack/kind-load-image.sh docker.io/bitinit/eureka latest
|
tools/hack/kind-load-image.sh docker.io/bitinit/eureka latest
|
||||||
# run-higress-e2e-test starts to run ingress e2e tests.
|
# run-higress-e2e-test starts to run ingress e2e tests.
|
||||||
|
|||||||
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
|
||||||
|
};
|
||||||
|
|
||||||
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
|
// Avoid pulling in incompatible libraries
|
||||||
replace github.com/docker/distribution => github.com/docker/distribution v0.0.0-20191216044856-a8371794149d
|
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
|
// 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
|
replace github.com/imdario/mergo => github.com/imdario/mergo v0.3.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/AlecAivazis/survey/v2 v2.3.6
|
||||||
github.com/agiledragon/gomonkey/v2 v2.9.0
|
github.com/agiledragon/gomonkey/v2 v2.9.0
|
||||||
github.com/avast/retry-go/v4 v4.3.4
|
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/go-zookeeper v1.0.4-0.20211212162352-f9d2183d89d5
|
||||||
github.com/dubbogo/gost v1.13.1
|
github.com/dubbogo/gost v1.13.1
|
||||||
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1
|
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/gogo/protobuf v1.3.2
|
||||||
github.com/golang/protobuf v1.5.2
|
github.com/golang/protobuf v1.5.2
|
||||||
github.com/google/go-cmp v0.5.9
|
github.com/google/go-cmp v0.5.9
|
||||||
@@ -28,32 +33,38 @@ require (
|
|||||||
github.com/hashicorp/consul/api v1.23.0
|
github.com/hashicorp/consul/api v1.23.0
|
||||||
github.com/hashicorp/go-multierror v1.1.1
|
github.com/hashicorp/go-multierror v1.1.1
|
||||||
github.com/hudl/fargo v1.4.0
|
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 v1.0.8
|
||||||
github.com/nacos-group/nacos-sdk-go/v2 v2.1.2
|
github.com/nacos-group/nacos-sdk-go/v2 v2.1.2
|
||||||
github.com/pkg/errors v0.9.1
|
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/pflag v1.0.5
|
||||||
|
github.com/spf13/viper v1.8.1
|
||||||
github.com/stretchr/testify v1.8.3
|
github.com/stretchr/testify v1.8.3
|
||||||
go.uber.org/atomic v1.9.0
|
go.uber.org/atomic v1.9.0
|
||||||
google.golang.org/grpc v1.48.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.v2 v2.4.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
istio.io/api v0.0.0-20211122181927-8da52c66ff23
|
istio.io/api v0.0.0-20211122181927-8da52c66ff23
|
||||||
istio.io/client-go v1.12.0-rc.1.0.20211118171212-b744b6f111e4
|
istio.io/client-go v1.12.0-rc.1.0.20211118171212-b744b6f111e4
|
||||||
istio.io/gogo-genproto v0.0.0-20211115195057-0e34bdd2be67
|
istio.io/gogo-genproto v0.0.0-20211115195057-0e34bdd2be67
|
||||||
istio.io/istio v0.0.0
|
istio.io/istio v0.0.0
|
||||||
istio.io/pkg v0.0.0-20211115195056-e379f31ee62a
|
istio.io/pkg v0.0.0-20211115195056-e379f31ee62a
|
||||||
k8s.io/api v0.22.2
|
k8s.io/api v0.24.1
|
||||||
k8s.io/apimachinery v0.22.2
|
k8s.io/apimachinery v0.24.1
|
||||||
k8s.io/cli-runtime v0.22.2
|
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
|
k8s.io/kubectl v0.22.2
|
||||||
sigs.k8s.io/controller-runtime v0.10.2
|
sigs.k8s.io/controller-runtime v0.10.2
|
||||||
sigs.k8s.io/yaml v1.3.0
|
sigs.k8s.io/yaml v1.3.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
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
|
cloud.google.com/go/logging v1.4.2 // indirect
|
||||||
contrib.go.opencensus.io/exporter/prometheus v0.4.0 // indirect
|
contrib.go.opencensus.io/exporter/prometheus v0.4.0 // indirect
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // 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/autorest/date v0.3.0 // indirect
|
||||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||||
github.com/Azure/go-autorest/tracing v0.6.0 // 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/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect
|
||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
||||||
github.com/Masterminds/sprig/v3 v3.2.2 // indirect
|
github.com/Masterminds/sprig/v3 v3.2.2 // indirect
|
||||||
github.com/Microsoft/go-winio v0.5.0 // indirect
|
github.com/Masterminds/squirrel v1.5.0 // indirect
|
||||||
github.com/Microsoft/hcsshim v0.8.21 // indirect
|
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||||
|
github.com/Microsoft/hcsshim v0.9.6 // indirect
|
||||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // 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/aliyun/alibaba-cloud-sdk-go v1.61.1704 // indirect
|
||||||
github.com/armon/go-metrics v0.4.1 // 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/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/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/census-instrumentation/opencensus-proto v0.3.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect
|
github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect
|
||||||
github.com/clbanning/mxj v1.8.4 // 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/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/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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0 // indirect
|
||||||
github.com/docker/cli v20.10.7+incompatible // indirect
|
github.com/distribution/distribution/v3 v3.0.0-20221201083218-92d136e113cf // indirect
|
||||||
github.com/docker/distribution v2.7.1+incompatible // indirect
|
github.com/docker/buildx v0.9.1 // indirect
|
||||||
github.com/docker/docker v20.10.7+incompatible // indirect
|
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||||
github.com/docker/docker-credential-helpers v0.6.3 // indirect
|
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
||||||
github.com/docker/go-units v0.4.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/envoyproxy/protoc-gen-validate v0.1.0 // indirect
|
||||||
github.com/evanphx/json-patch v4.12.0+incompatible // 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/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/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.5.1 // 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/ghodss/yaml v1.0.0 // indirect
|
||||||
github.com/go-errors/errors v1.0.1 // indirect
|
github.com/go-errors/errors v1.0.1 // indirect
|
||||||
github.com/go-kit/log v0.1.0 // indirect
|
github.com/go-kit/log v0.1.0 // indirect
|
||||||
github.com/go-logfmt/logfmt v0.5.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/jsonpointer v0.19.5 // indirect
|
||||||
github.com/go-openapi/jsonreference 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/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-jwt/jwt/v4 v4.2.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/golang/mock v1.6.0 // indirect
|
github.com/golang/mock v1.6.0 // indirect
|
||||||
@@ -118,23 +147,35 @@ require (
|
|||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.1.1 // indirect
|
github.com/googleapis/gax-go/v2 v2.1.1 // indirect
|
||||||
github.com/googleapis/gnostic v0.5.5 // 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/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // 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/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||||
github.com/hashicorp/go-hclog v1.5.0 // indirect
|
github.com/hashicorp/go-hclog v1.5.0 // indirect
|
||||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||||
github.com/hashicorp/go-rootcerts v1.0.2 // 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/golang-lru v0.5.4 // indirect
|
||||||
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/hashicorp/serf v0.10.1 // indirect
|
github.com/hashicorp/serf v0.10.1 // indirect
|
||||||
github.com/huandu/xstrings v1.3.2 // indirect
|
github.com/huandu/xstrings v1.3.2 // indirect
|
||||||
github.com/imdario/mergo v0.3.12 // indirect
|
github.com/imdario/mergo v0.3.13 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 // 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/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/jonboulle/clockwork v0.2.2 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // 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/backoff/v2 v2.0.7 // indirect
|
||||||
github.com/lestrrat-go/blackmagic v1.0.0 // indirect
|
github.com/lestrrat-go/blackmagic v1.0.0 // indirect
|
||||||
github.com/lestrrat-go/httpcc 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/option v1.0.0 // indirect
|
||||||
github.com/lestrrat/go-file-rotatelogs v0.0.0-20180223000712-d3151e2a480f // 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/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/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-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.17 // 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/dns v1.1.43 // indirect
|
||||||
|
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||||
github.com/mitchellh/copystructure v1.2.0 // 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/go-wordwrap v1.0.0 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 // 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/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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // 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/natefinch/lumberjack v2.0.0+incompatible // indirect
|
||||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect
|
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.0.1 // indirect
|
github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
|
||||||
github.com/opencontainers/runc v1.0.2 // indirect
|
github.com/opencontainers/runc v1.1.3 // indirect
|
||||||
github.com/openshift/api v0.0.0-20200713203337-b2494ecb17dd // 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/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/prometheus/client_golang v1.12.2 // 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/common v0.32.1 // indirect
|
||||||
github.com/prometheus/procfs v0.7.3 // indirect
|
github.com/prometheus/procfs v0.7.3 // indirect
|
||||||
github.com/prometheus/statsd_exporter v0.21.0 // 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/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/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/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/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/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect
|
||||||
github.com/yl2chen/cidranger v1.0.2 // indirect
|
github.com/yl2chen/cidranger v1.0.2 // indirect
|
||||||
go.opencensus.io v0.23.0 // 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.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
|
||||||
go.uber.org/multierr v1.7.0 // indirect
|
go.uber.org/multierr v1.7.0 // indirect
|
||||||
go.uber.org/zap v1.21.0 // indirect
|
go.uber.org/zap v1.21.0 // indirect
|
||||||
golang.org/x/crypto v0.11.0 // indirect
|
golang.org/x/crypto v0.11.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
|
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
|
||||||
golang.org/x/net v0.12.0 // 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/sync v0.2.0 // indirect
|
||||||
golang.org/x/sys v0.10.0 // indirect
|
golang.org/x/sys v0.10.0 // indirect
|
||||||
golang.org/x/term 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/v2 v2.2.0 // indirect
|
||||||
gomodules.xyz/jsonpatch/v3 v3.0.1 // indirect
|
gomodules.xyz/jsonpatch/v3 v3.0.1 // indirect
|
||||||
gomodules.xyz/orderedmap v0.1.0 // 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/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/gcfg.v1 v1.2.3 // indirect
|
||||||
|
gopkg.in/gorp.v1 v1.7.2 // indirect
|
||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
gopkg.in/ini.v1 v1.66.2 // indirect
|
gopkg.in/ini.v1 v1.66.2 // indirect
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
k8s.io/apiserver v0.22.5 // indirect
|
||||||
k8s.io/apiextensions-apiserver v0.22.2 // indirect
|
k8s.io/component-base v0.22.5 // indirect
|
||||||
k8s.io/component-base v0.22.2 // indirect
|
k8s.io/klog/v2 v2.60.1 // indirect
|
||||||
k8s.io/klog/v2 v2.10.0 // indirect
|
k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c // indirect
|
||||||
k8s.io/kube-openapi v0.0.0-20211020163157-7327e2aaee2b // indirect
|
|
||||||
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // 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/gateway-api v0.4.0 // indirect
|
||||||
sigs.k8s.io/kustomize/api v0.8.11 // indirect
|
sigs.k8s.io/kustomize/api v0.8.11 // indirect
|
||||||
sigs.k8s.io/kustomize/kyaml v0.11.0 // 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/client-go => ./external/client-go
|
||||||
|
|
||||||
replace istio.io/istio => ./external/istio
|
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
|
apiVersion: v2
|
||||||
appVersion: 1.1.2
|
appVersion: 1.3.0
|
||||||
description: Helm chart for deploying higress gateways
|
description: Helm chart for deploying higress gateways
|
||||||
icon: https://higress.io/img/higress_logo_small.png
|
icon: https://higress.io/img/higress_logo_small.png
|
||||||
home: http://higress.io/
|
home: http://higress.io/
|
||||||
@@ -10,4 +10,4 @@ name: higress-core
|
|||||||
sources:
|
sources:
|
||||||
- http://github.com/alibaba/higress
|
- http://github.com/alibaba/higress
|
||||||
type: application
|
type: application
|
||||||
version: 1.1.2
|
version: 1.3.0
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
configSources:
|
configSources:
|
||||||
- address: "xds://127.0.0.1:15051"
|
- address: "xds://127.0.0.1:15051"
|
||||||
{{- if .Values.global.enableIstioAPI }}
|
{{- if or .Values.global.enableIstioAPI .Values.global.enableGatewayAPI }}
|
||||||
- address: "k8s://"
|
- address: "k8s://"
|
||||||
{{- end }}
|
{{- 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"]
|
- 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"]
|
verbs: ["get", "watch", "list"]
|
||||||
resources: ["*"]
|
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
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
name: {{ include "controller.name" . }}
|
name: {{ include "controller.name" . }}
|
||||||
|
namespace: {{ .Release.Namespace }}
|
||||||
labels:
|
labels:
|
||||||
{{- include "controller.labels" . | nindent 4 }}
|
{{- include "controller.labels" . | nindent 4 }}
|
||||||
spec:
|
spec:
|
||||||
@@ -126,10 +127,19 @@ spec:
|
|||||||
value: "{{ .Values.global.istiod.enableAnalysis }}"
|
value: "{{ .Values.global.istiod.enableAnalysis }}"
|
||||||
- name: CLUSTER_ID
|
- name: CLUSTER_ID
|
||||||
value: "{{ $.Values.global.multiCluster.clusterName | default `Kubernetes` }}"
|
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 }}
|
{{- if .Values.global.enableIstioAPI }}
|
||||||
- name: HIGRESS_ENABLE_ISTIO_API
|
- name: HIGRESS_ENABLE_ISTIO_API
|
||||||
value: "true"
|
value: "true"
|
||||||
{{- end }}
|
{{- 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 }}
|
{{- if not .Values.global.enableHigressIstio }}
|
||||||
- name: CUSTOM_CA_CERT_NAME
|
- name: CUSTOM_CA_CERT_NAME
|
||||||
value: "higress-ca-root-cert"
|
value: "higress-ca-root-cert"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ apiVersion: v1
|
|||||||
kind: Service
|
kind: Service
|
||||||
metadata:
|
metadata:
|
||||||
name: {{ include "controller.name" . }}
|
name: {{ include "controller.name" . }}
|
||||||
|
namespace: {{ .Release.Namespace }}
|
||||||
labels:
|
labels:
|
||||||
{{- include "controller.labels" . | nindent 4 }}
|
{{- include "controller.labels" . | nindent 4 }}
|
||||||
spec:
|
spec:
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ global:
|
|||||||
local: false # When deploying to a local cluster (e.g.: kind cluster), set this to true.
|
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.
|
kind: false # Deprecated. Please use "global.local" instead. Will be removed later.
|
||||||
enableIstioAPI: false
|
enableIstioAPI: false
|
||||||
|
enableGatewayAPI: false
|
||||||
# Deprecated
|
# Deprecated
|
||||||
enableHigressIstio: false
|
enableHigressIstio: false
|
||||||
# Used to locate istiod.
|
# Used to locate istiod.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
- name: higress-core
|
- name: higress-core
|
||||||
repository: file://../core
|
repository: file://../core
|
||||||
version: 1.1.2
|
version: 1.3.0
|
||||||
- name: higress-console
|
- name: higress-console
|
||||||
repository: https://higress.io/helm-charts/
|
repository: https://higress.io/helm-charts/
|
||||||
version: 1.1.2
|
version: 1.3.0
|
||||||
digest: sha256:8fc099c4ad77bcdc4b9dde2ef14f89b2159b6fdcc49a3dc7e1cccb01a7ed99b9
|
digest: sha256:3efc59ad8cd92ab4c3c87abeed8e2fc0288bb3ecc2805888ba6eaaf265ba6a10
|
||||||
generated: "2023-09-19T21:46:20.2567789+08:00"
|
generated: "2023-11-02T11:45:56.011629+08:00"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
appVersion: 1.1.2
|
appVersion: 1.3.0
|
||||||
description: Helm chart for deploying Higress gateways
|
description: Helm chart for deploying Higress gateways
|
||||||
icon: https://higress.io/img/higress_logo_small.png
|
icon: https://higress.io/img/higress_logo_small.png
|
||||||
home: http://higress.io/
|
home: http://higress.io/
|
||||||
@@ -12,9 +12,9 @@ sources:
|
|||||||
dependencies:
|
dependencies:
|
||||||
- name: higress-core
|
- name: higress-core
|
||||||
repository: "file://../core"
|
repository: "file://../core"
|
||||||
version: 1.1.2
|
version: 1.3.0
|
||||||
- name: higress-console
|
- name: higress-console
|
||||||
repository: "https://higress.io/helm-charts/"
|
repository: "https://higress.io/helm-charts/"
|
||||||
version: 1.1.2
|
version: 1.3.0
|
||||||
type: application
|
type: application
|
||||||
version: 1.1.2
|
version: 1.3.0
|
||||||
|
|||||||
@@ -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.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.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.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 |
|
| global.istioNamespace | The namespace istio is installed to | istio-system |
|
||||||
| **Core Paramters** | | |
|
| **Core Paramters** | | |
|
||||||
| higress-core.gateway.replicas | Number of Higress Gateway pods | 2 |
|
| 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)
|
||||||
@@ -20,6 +20,10 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"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"
|
prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/reflection"
|
"google.golang.org/grpc/reflection"
|
||||||
@@ -46,11 +50,6 @@ import (
|
|||||||
"istio.io/pkg/log"
|
"istio.io/pkg/log"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
"k8s.io/client-go/tools/cache"
|
"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 {
|
type XdsOptions struct {
|
||||||
@@ -225,9 +224,12 @@ func (s *Server) initConfigController() error {
|
|||||||
if options.ClusterId == "Kubernetes" {
|
if options.ClusterId == "Kubernetes" {
|
||||||
options.ClusterId = ""
|
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)
|
s.configStores = append(s.configStores, ingressConfig)
|
||||||
|
|
||||||
// Wrap the config controller with a cache.
|
// Wrap the config controller with a cache.
|
||||||
aggregateConfigController, err := configaggregate.MakeCache(s.configStores)
|
aggregateConfigController, err := configaggregate.MakeCache(s.configStores)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -242,7 +244,7 @@ func (s *Server) initConfigController() error {
|
|||||||
|
|
||||||
// Defer starting the controller until after the service is created.
|
// Defer starting the controller until after the service is created.
|
||||||
s.server.RunComponent(func(stop <-chan struct{}) error {
|
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
|
return err
|
||||||
}
|
}
|
||||||
go s.configController.Run(stop)
|
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"
|
||||||
|
)
|
||||||
@@ -33,8 +33,8 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
adminPort = 15000
|
defaultProxyAdminPort = 15000
|
||||||
containerName = "envoy"
|
containerName = "envoy"
|
||||||
)
|
)
|
||||||
|
|
||||||
func retrieveConfigDump(args []string, includeEds bool) ([]byte, error) {
|
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)
|
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)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -34,6 +34,7 @@ type fakePortForwarder struct {
|
|||||||
localPort int
|
localPort int
|
||||||
l net.Listener
|
l net.Listener
|
||||||
mux *http.ServeMux
|
mux *http.ServeMux
|
||||||
|
stopCh chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newFakePortForwarder(b []byte) (kubernetes.PortForwarder, error) {
|
func newFakePortForwarder(b []byte) (kubernetes.PortForwarder, error) {
|
||||||
@@ -46,6 +47,7 @@ func newFakePortForwarder(b []byte) (kubernetes.PortForwarder, error) {
|
|||||||
responseBody: b,
|
responseBody: b,
|
||||||
localPort: p,
|
localPort: p,
|
||||||
mux: http.NewServeMux(),
|
mux: http.NewServeMux(),
|
||||||
|
stopCh: make(chan struct{}),
|
||||||
}
|
}
|
||||||
fw.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
fw.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
_, _ = w.Write(fw.responseBody)
|
_, _ = w.Write(fw.responseBody)
|
||||||
@@ -54,6 +56,10 @@ func newFakePortForwarder(b []byte) (kubernetes.PortForwarder, error) {
|
|||||||
return fw, nil
|
return fw, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fw *fakePortForwarder) WaitForStop() {
|
||||||
|
<-fw.stopCh
|
||||||
|
}
|
||||||
|
|
||||||
func (fw *fakePortForwarder) Start() error {
|
func (fw *fakePortForwarder) Start() error {
|
||||||
l, err := net.Listen("tcp", fw.Address())
|
l, err := net.Listen("tcp", fw.Address())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
387
pkg/cmd/hgctl/dashboard.go
Normal file
387
pkg/cmd/hgctl/dashboard.go
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
// 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"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/options"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"k8s.io/apimachinery/pkg/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
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
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", "l", "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.")
|
||||||
|
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 {
|
||||||
|
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.kubernetes.io/name=higress-console")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("not able to locate console pod: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pl.Items) < 1 {
|
||||||
|
return errors.New("no higress console pods found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
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) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
fmt.Fprintf(writer, "%s\n", url)
|
||||||
|
|
||||||
|
if !browser {
|
||||||
|
fmt.Fprint(writer, "skipping opening a browser")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "linux":
|
||||||
|
err = exec.Command("xdg-open", url).Start()
|
||||||
|
case "windows":
|
||||||
|
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
|
||||||
|
case "darwin":
|
||||||
|
err = exec.Command("open", url).Start()
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(writer, "Unsupported platform %q; open %s in your browser.\n", runtime.GOOS, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(writer, "Failed to open browser; open %s in your browser.\nError: %s\n", url, 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{})
|
||||||
|
}
|
||||||
327
pkg/cmd/hgctl/helm/common.go
Normal file
327
pkg/cmd/hgctl/helm/common.go
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
344
pkg/cmd/hgctl/helm/profile.go
Normal file
344
pkg/cmd/hgctl/helm/profile.go
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
// 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"`
|
||||||
|
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
|
||||||
|
}
|
||||||
663
pkg/cmd/hgctl/helm/render.go
Normal file
663
pkg/cmd/hgctl/helm/render.go
Normal file
@@ -0,0 +1,663 @@
|
|||||||
|
// 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"
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
}
|
||||||
|
// TODO need to specify k8s version
|
||||||
|
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.Render(cht, resVals)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
200
pkg/cmd/hgctl/install.go
Normal file
200
pkg/cmd/hgctl/install.go
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
// 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/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, "🧐 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)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
112
pkg/cmd/hgctl/installer/component.go
Normal file
112
pkg/cmd/hgctl/installer/component.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 (
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/util"
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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
|
||||||
|
}
|
||||||
108
pkg/cmd/hgctl/installer/gateway_api.go
Normal file
108
pkg/cmd/hgctl/installer/gateway_api.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package installer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGatewayAPIComponent(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),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gatewayAPIComponent := &GatewayAPIComponent{
|
||||||
|
profile: profile,
|
||||||
|
renderer: renderer,
|
||||||
|
opts: newOpts,
|
||||||
|
writer: writer,
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
120
pkg/cmd/hgctl/installer/higress.go
Normal file
120
pkg/cmd/hgctl/installer/higress.go
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
// 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"
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Higress ComponentName = "higress"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HigressComponent struct {
|
||||||
|
profile *helm.Profile
|
||||||
|
started bool
|
||||||
|
opts *ComponentOptions
|
||||||
|
renderer helm.Renderer
|
||||||
|
writer io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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(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),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
higressComponent := &HigressComponent{
|
||||||
|
profile: profile,
|
||||||
|
renderer: renderer,
|
||||||
|
opts: newOpts,
|
||||||
|
writer: writer,
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
101
pkg/cmd/hgctl/installer/installer_docker.go
Normal file
101
pkg/cmd/hgctl/installer/installer_docker.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
// 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"
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/util"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DockerInstaller struct {
|
||||||
|
started bool
|
||||||
|
standalone *StandaloneComponent
|
||||||
|
profile *helm.Profile
|
||||||
|
writer io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DockerInstaller) Install() error {
|
||||||
|
fmt.Fprintf(d.writer, "\n⌛️ Processing installation... \n\n")
|
||||||
|
|
||||||
|
if err := d.standalone.Install(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
profileName, _ := GetInstalledYamlPath()
|
||||||
|
fmt.Fprintf(d.writer, "\n✔️ Wrote Profile: \"%s\" \n", profileName)
|
||||||
|
if err := util.WriteFileString(profileName, util.ToYAML(d.profile), 0o644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
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, _ := GetInstalledYamlPath()
|
||||||
|
fmt.Fprintf(d.writer, "\n✔️ Removed Profile: \"%s\" \n", profileName)
|
||||||
|
os.Remove(profileName)
|
||||||
|
|
||||||
|
fmt.Fprintf(d.writer, "\n🎊 Uninstall All Resources Complete!\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
op := &DockerInstaller{
|
||||||
|
profile: profile,
|
||||||
|
standalone: standaloneComponent,
|
||||||
|
writer: writer,
|
||||||
|
}
|
||||||
|
return op, nil
|
||||||
|
}
|
||||||
307
pkg/cmd/hgctl/installer/installer_k8s.go
Normal file
307
pkg/cmd/hgctl/installer/installer_k8s.go
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
// 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"
|
||||||
|
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 := 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 installation... \n\n")
|
||||||
|
if err := o.ApplyManifests(manifestMap); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
profileName, _ := GetInstalledYamlPath()
|
||||||
|
fmt.Fprintf(o.writer, "\n✔️ Wrote Profile: \"%s\" \n", profileName)
|
||||||
|
if err := util.WriteFileString(profileName, util.ToYAML(o.profile), 0o644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
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, _ := GetInstalledYamlPath()
|
||||||
|
fmt.Fprintf(o.writer, "\n✔️ Removed Profile: \"%s\" \n", profileName)
|
||||||
|
os.Remove(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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 components
|
||||||
|
components := make(map[ComponentName]Component)
|
||||||
|
opts := []ComponentOption{
|
||||||
|
WithComponentNamespace(profile.Global.Namespace),
|
||||||
|
WithComponentChartPath(profile.InstallPackagePath),
|
||||||
|
WithComponentVersion(profile.Charts.Higress.Version),
|
||||||
|
WithComponentRepoURL(profile.Charts.Higress.Url),
|
||||||
|
WithComponentChartName(profile.Charts.Higress.Name),
|
||||||
|
}
|
||||||
|
if quiet {
|
||||||
|
opts = append(opts, WithQuiet())
|
||||||
|
}
|
||||||
|
higressComponent, err := NewHigressComponent(profile, writer, opts...)
|
||||||
|
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"),
|
||||||
|
}
|
||||||
|
if quiet {
|
||||||
|
opts = append(opts, WithQuiet())
|
||||||
|
}
|
||||||
|
|
||||||
|
istioCRDComponent, err := NewIstioCRDComponent(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"),
|
||||||
|
}
|
||||||
|
if quiet {
|
||||||
|
opts = append(opts, WithQuiet())
|
||||||
|
}
|
||||||
|
|
||||||
|
gatewayAPIComponent, err := NewGatewayAPIComponent(profile, writer, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("NewGatewayAPIComponent failed, err: %s", err)
|
||||||
|
}
|
||||||
|
components[GatewayAPI] = gatewayAPIComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
op := &K8sInstaller{
|
||||||
|
profile: profile,
|
||||||
|
components: components,
|
||||||
|
kubeCli: cli,
|
||||||
|
writer: writer,
|
||||||
|
}
|
||||||
|
return op, nil
|
||||||
|
}
|
||||||
118
pkg/cmd/hgctl/installer/istio.go
Normal file
118
pkg/cmd/hgctl/installer/istio.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
// 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/manifests"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Istio ComponentName = "istio"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IstioCRDComponent struct {
|
||||||
|
profile *helm.Profile
|
||||||
|
started bool
|
||||||
|
opts *ComponentOptions
|
||||||
|
renderer helm.Renderer
|
||||||
|
writer io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIstioCRDComponent(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),
|
||||||
|
)
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
istioComponent := &IstioCRDComponent{
|
||||||
|
profile: profile,
|
||||||
|
renderer: renderer,
|
||||||
|
opts: newOpts,
|
||||||
|
writer: writer,
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
139
pkg/cmd/hgctl/installer/standalone.go
Normal file
139
pkg/cmd/hgctl/installer/standalone.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 _, err := GetProfileInstalledPath(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
kubescheme "k8s.io/client-go/kubernetes/scheme"
|
kubescheme "k8s.io/client-go/kubernetes/scheme"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
"k8s.io/client-go/tools/clientcmd"
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
"k8s.io/client-go/tools/remotecommand"
|
"k8s.io/client-go/tools/remotecommand"
|
||||||
|
"k8s.io/client-go/util/retry"
|
||||||
|
ctrClient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CLIClient interface {
|
type CLIClient interface {
|
||||||
@@ -44,6 +48,15 @@ type CLIClient interface {
|
|||||||
|
|
||||||
// PodExec takes a command and the pod data to run the command in the specified pod.
|
// 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)
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ CLIClient = &client{}
|
var _ CLIClient = &client{}
|
||||||
@@ -52,6 +65,7 @@ type client struct {
|
|||||||
config *rest.Config
|
config *rest.Config
|
||||||
restClient *rest.RESTClient
|
restClient *rest.RESTClient
|
||||||
kube kubernetes.Interface
|
kube kubernetes.Interface
|
||||||
|
ctrClient ctrClient.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCLIClient(clientConfig clientcmd.ClientConfig) (CLIClient, error) {
|
func NewCLIClient(clientConfig clientcmd.ClientConfig) (CLIClient, error) {
|
||||||
@@ -80,33 +94,13 @@ func newClientInternal(clientConfig clientcmd.ClientConfig) (*client, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.ctrClient, err = ctrClient.New(c.config, ctrClient.Options{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return &c, 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 {
|
func (c *client) RESTConfig() *rest.Config {
|
||||||
if c.config == nil {
|
if c.config == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -170,3 +164,85 @@ func (c *client) PodExec(namespacedName types.NamespacedName, container string,
|
|||||||
stderr = stderrBuf.String()
|
stderr = stderrBuf.String()
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
@@ -48,6 +48,9 @@ type PortForwarder interface {
|
|||||||
|
|
||||||
// Address returns the address of the local forwarded address.
|
// Address returns the address of the local forwarded address.
|
||||||
Address() string
|
Address() string
|
||||||
|
|
||||||
|
// WaitForStop blocks until connection closed (e.g. control-C interrupt)
|
||||||
|
WaitForStop()
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ PortForwarder = &localForwarder{}
|
var _ PortForwarder = &localForwarder{}
|
||||||
@@ -153,3 +156,7 @@ func (f *localForwarder) Stop() {
|
|||||||
func (f *localForwarder) Address() string {
|
func (f *localForwarder) Address() string {
|
||||||
return fmt.Sprintf("%s:%d", DefaultLocalAddress, f.localPort)
|
return fmt.Sprintf("%s:%d", DefaultLocalAddress, 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
|
||||||
|
}
|
||||||
296
pkg/cmd/hgctl/plugin/init/templates.go
Normal file
296
pkg/cmd/hgctl/plugin/init/templates.go
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package plugininit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/AlecAivazis/survey/v2"
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
goMain = `// File generated by hgctl. Modify as required.
|
||||||
|
// See: https://higress.io/zh-cn/docs/user/wasm-go#2-%E7%BC%96%E5%86%99-maingo-%E6%96%87%E4%BB%B6
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
|
||||||
|
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
|
||||||
|
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
wrapper.SetCtx(
|
||||||
|
"{{ .Name }}",
|
||||||
|
wrapper.ParseConfigBy(parseConfig),
|
||||||
|
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Name {{ .Name }}
|
||||||
|
// @Category {{ .Category }}
|
||||||
|
// @Phase {{ .Phase }}
|
||||||
|
// @Priority {{ .Priority }}
|
||||||
|
// @Title {{ .I18nType }} {{ .Title }}
|
||||||
|
// @Description {{ .I18nType }} {{ .Description }}
|
||||||
|
// @IconUrl {{ .IconUrl }}
|
||||||
|
// @Version {{ .Version }}
|
||||||
|
//
|
||||||
|
// @Contact.name {{ .ContactName }}
|
||||||
|
// @Contact.url {{ .ContactUrl }}
|
||||||
|
// @Contact.email {{ .ContactEmail }}
|
||||||
|
//
|
||||||
|
// @Example
|
||||||
|
// firstField: hello
|
||||||
|
// secondField: world
|
||||||
|
// @End
|
||||||
|
//
|
||||||
|
type PluginConfig struct {
|
||||||
|
// @Title 第一个字段,注解格式为 @Title [语言] [标题],语言缺省值为 en-US
|
||||||
|
// @Description 字符串的前半部分,注解格式为 @Description [语言] [描述],语言缺省值为 en-US
|
||||||
|
firstField string ` + "`required:\"true\"`" + `
|
||||||
|
|
||||||
|
// @Title en-US Second Field, annotation format is @Title [language] [title], language defaults to en-US
|
||||||
|
// @Description en-US The second half of the string, annotation format is @Description [language] [description], language defaults to en-US
|
||||||
|
secondField string ` + "`required:\"true\"`" + `
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseConfig(json gjson.Result, config *PluginConfig, log wrapper.Log) error {
|
||||||
|
config.firstField = json.Get("firstField").String()
|
||||||
|
config.secondField = json.Get("secondField").String()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig, log wrapper.Log) types.Action {
|
||||||
|
err := proxywasm.AddHttpRequestHeader(config.firstField, config.secondField)
|
||||||
|
if err != nil {
|
||||||
|
log.Critical("failed to set request header")
|
||||||
|
}
|
||||||
|
return types.ActionContinue
|
||||||
|
}
|
||||||
|
`
|
||||||
|
goMod = `// File generated by hgctl. Modify as required.
|
||||||
|
|
||||||
|
module {{ .Name }}
|
||||||
|
|
||||||
|
go 1.19
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alibaba/higress/plugins/wasm-go v0.0.0-20231019123123-86b223bc75f1
|
||||||
|
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0
|
||||||
|
github.com/tidwall/gjson v1.14.3
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
gitIgnore = `# File generated by hgctl. Modify as required.
|
||||||
|
|
||||||
|
*
|
||||||
|
|
||||||
|
!/.gitignore
|
||||||
|
|
||||||
|
!*.go
|
||||||
|
!go.sum
|
||||||
|
!go.mod
|
||||||
|
|
||||||
|
!LICENSE
|
||||||
|
!*.md
|
||||||
|
!*.yaml
|
||||||
|
!*.yml
|
||||||
|
|
||||||
|
!*/
|
||||||
|
|
||||||
|
/out
|
||||||
|
/test
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
func genGoMain(ans *answer, dir string) error {
|
||||||
|
path := fmt.Sprintf("%s/main.go", dir)
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if err = template.Must(template.New("GoMain").Parse(goMain)).Execute(f, ans); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func genGoMod(ans *answer, dir string) error {
|
||||||
|
path := fmt.Sprintf("%s/go.mod", dir)
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if err = template.Must(template.New("GoMod").Parse(goMod)).Execute(f, ans); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func genGitIgnore(dir string) error {
|
||||||
|
path := fmt.Sprintf("%s/.gitignore", dir)
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if _, err = f.WriteString(gitIgnore); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// obtain parameters through command line interaction
|
||||||
|
type answer struct {
|
||||||
|
Name string
|
||||||
|
Category string
|
||||||
|
Phase string
|
||||||
|
Priority int64
|
||||||
|
I18nType string
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
IconUrl string
|
||||||
|
Version string
|
||||||
|
|
||||||
|
ContactName string
|
||||||
|
ContactUrl string
|
||||||
|
ContactEmail string
|
||||||
|
}
|
||||||
|
|
||||||
|
var questions = []*survey.Question{
|
||||||
|
{
|
||||||
|
Name: "Name",
|
||||||
|
Prompt: &survey.Input{
|
||||||
|
Message: "Plugin name:",
|
||||||
|
Default: "hello-world",
|
||||||
|
},
|
||||||
|
Validate: survey.Required,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Category",
|
||||||
|
Prompt: &survey.Select{
|
||||||
|
Message: "Choose a plugin category:",
|
||||||
|
Options: []string{
|
||||||
|
string(types.CategoryCustom),
|
||||||
|
string(types.CategoryAuth),
|
||||||
|
string(types.CategorySecurity),
|
||||||
|
string(types.CategoryProtocol),
|
||||||
|
string(types.CategoryFlowControl),
|
||||||
|
string(types.CategoryFlowMonitor),
|
||||||
|
},
|
||||||
|
Default: string(types.CategoryCustom),
|
||||||
|
},
|
||||||
|
Validate: survey.Required,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Phase",
|
||||||
|
Prompt: &survey.Select{
|
||||||
|
Message: "Choose a execution phase:",
|
||||||
|
Options: []string{
|
||||||
|
string(types.PhaseUnspecified),
|
||||||
|
string(types.PhaseAuthn),
|
||||||
|
string(types.PhaseAuthz),
|
||||||
|
string(types.PhaseStats),
|
||||||
|
},
|
||||||
|
Default: string(types.PhaseUnspecified),
|
||||||
|
},
|
||||||
|
Validate: survey.Required,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Priority",
|
||||||
|
Prompt: &survey.Input{
|
||||||
|
Message: "Execution priority:",
|
||||||
|
Default: "0",
|
||||||
|
},
|
||||||
|
Validate: survey.Required,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "I18nType",
|
||||||
|
Prompt: &survey.Select{
|
||||||
|
Message: "Choose a language:",
|
||||||
|
Options: []string{
|
||||||
|
string(types.I18nEN_US),
|
||||||
|
string(types.I18nZH_CN),
|
||||||
|
},
|
||||||
|
Default: string(types.I18nDefault),
|
||||||
|
},
|
||||||
|
Validate: survey.Required,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Title",
|
||||||
|
Prompt: &survey.Input{
|
||||||
|
Message: "Display name in the plugin market:",
|
||||||
|
Default: "Hello World",
|
||||||
|
},
|
||||||
|
Validate: survey.Required,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Description",
|
||||||
|
Prompt: &survey.Input{
|
||||||
|
Message: "Description of the plugin functionality:",
|
||||||
|
Default: "This is a demo plugin",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "IconUrl",
|
||||||
|
Prompt: &survey.Input{
|
||||||
|
Message: "Display icon in the plugin market:",
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Version",
|
||||||
|
Prompt: &survey.Input{
|
||||||
|
Message: "Plugin version:",
|
||||||
|
Default: "0.1.0",
|
||||||
|
},
|
||||||
|
Validate: survey.Required,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "ContactName",
|
||||||
|
Prompt: &survey.Input{
|
||||||
|
Message: "Name of developer:",
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "ContactUrl",
|
||||||
|
Prompt: &survey.Input{
|
||||||
|
Message: "Homepage of developer:",
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "ContactEmail",
|
||||||
|
Prompt: &survey.Input{
|
||||||
|
Message: "Email of developer:",
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
744
pkg/cmd/hgctl/plugin/install/asker.go
Normal file
744
pkg/cmd/hgctl/plugin/install/asker.go
Normal file
@@ -0,0 +1,744 @@
|
|||||||
|
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package install
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/types"
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/utils"
|
||||||
|
|
||||||
|
"github.com/AlecAivazis/survey/v2"
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/santhosh-tekuri/jsonschema/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
askInterrupted = "X Interrupted."
|
||||||
|
invalidSyntax = "X Invalid syntax."
|
||||||
|
failedToValidate = "X Failed to validate: not satisfied with schema."
|
||||||
|
addConfSuccessful = "√ Successful to add configuration."
|
||||||
|
)
|
||||||
|
|
||||||
|
var iconIdent = strings.Repeat(" ", 2)
|
||||||
|
|
||||||
|
type Asker interface {
|
||||||
|
Ask() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type WasmPluginSpecConfAsker struct {
|
||||||
|
resp *WasmPluginSpecConf
|
||||||
|
|
||||||
|
ingAsk *IngressAsker
|
||||||
|
domAsk *DomainAsker
|
||||||
|
glcAsk *GlobalConfAsker
|
||||||
|
|
||||||
|
printer *utils.YesOrNoPrinter
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWasmPluginSpecConfAsker(ingAsk *IngressAsker, domAsk *DomainAsker, glcAsk *GlobalConfAsker, printer *utils.YesOrNoPrinter) *WasmPluginSpecConfAsker {
|
||||||
|
return &WasmPluginSpecConfAsker{
|
||||||
|
ingAsk: ingAsk,
|
||||||
|
domAsk: domAsk,
|
||||||
|
glcAsk: glcAsk,
|
||||||
|
printer: printer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *WasmPluginSpecConfAsker) Ask() error {
|
||||||
|
var (
|
||||||
|
wpc = NewPluginSpecConf()
|
||||||
|
|
||||||
|
globalConf map[string]interface{}
|
||||||
|
ingressRule *IngressMatchRule
|
||||||
|
domainRule *DomainMatchRule
|
||||||
|
|
||||||
|
scopeA = newScopeAsker(p.printer)
|
||||||
|
rewriteA = newRewriteAsker(p.printer)
|
||||||
|
ruleA = newRuleAsker(p.printer)
|
||||||
|
|
||||||
|
complete = false
|
||||||
|
)
|
||||||
|
|
||||||
|
for {
|
||||||
|
err := scopeA.Ask()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
scope := scopeA.resp
|
||||||
|
|
||||||
|
switch scope {
|
||||||
|
case types.ScopeInstance:
|
||||||
|
err = ruleA.Ask()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rule := ruleA.resp
|
||||||
|
|
||||||
|
switch rule {
|
||||||
|
case ruleIngress:
|
||||||
|
if ingressRule != nil {
|
||||||
|
p.printer.Yesf("\n%s\n", ingressRule)
|
||||||
|
err = rewriteA.Ask()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !rewriteA.resp {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.ingAsk.scope = scope
|
||||||
|
err = p.ingAsk.Ask()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ingressRule = p.ingAsk.resp
|
||||||
|
|
||||||
|
case ruleDomain:
|
||||||
|
if domainRule != nil {
|
||||||
|
p.printer.Yesf("\n%s\n", domainRule)
|
||||||
|
err = rewriteA.Ask()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !rewriteA.resp {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.domAsk.scope = scope
|
||||||
|
err = p.domAsk.Ask()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
domainRule = p.domAsk.resp
|
||||||
|
}
|
||||||
|
|
||||||
|
case types.ScopeGlobal:
|
||||||
|
if globalConf != nil {
|
||||||
|
b, _ := utils.MarshalYamlWithIndent(globalConf, 2)
|
||||||
|
p.printer.Yesf("\n%s\n", string(b))
|
||||||
|
err = rewriteA.Ask()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !rewriteA.resp {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.glcAsk.scope = scope
|
||||||
|
err = p.glcAsk.Ask()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
globalConf = p.glcAsk.resp
|
||||||
|
|
||||||
|
case "Complete":
|
||||||
|
complete = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if complete {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalConf != nil {
|
||||||
|
wpc.DefaultConfig = globalConf
|
||||||
|
}
|
||||||
|
if ingressRule != nil {
|
||||||
|
wpc.MatchRules = append(wpc.MatchRules, ingressRule)
|
||||||
|
}
|
||||||
|
if domainRule != nil {
|
||||||
|
wpc.MatchRules = append(wpc.MatchRules, domainRule)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.printer.Yesln("The complete configuration is as follows:")
|
||||||
|
p.printer.Yesf("\n%s\n", wpc)
|
||||||
|
p.resp = wpc
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type IngressAsker struct {
|
||||||
|
resp *IngressMatchRule
|
||||||
|
|
||||||
|
structName string
|
||||||
|
schema *types.JSONSchemaProps
|
||||||
|
scope types.Scope
|
||||||
|
|
||||||
|
vld *jsonschema.Schema // for validation
|
||||||
|
printer *utils.YesOrNoPrinter
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIngressAsker(structName string, schema *types.JSONSchemaProps, vld *jsonschema.Schema, printer *utils.YesOrNoPrinter) *IngressAsker {
|
||||||
|
return &IngressAsker{
|
||||||
|
structName: structName,
|
||||||
|
schema: schema,
|
||||||
|
vld: vld,
|
||||||
|
printer: printer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IngressAsker) Ask() error {
|
||||||
|
continueA := newContinueAsker(i.printer)
|
||||||
|
ings := make([]string, 0)
|
||||||
|
for {
|
||||||
|
var ing string
|
||||||
|
err := utils.AskOne(&survey.Input{
|
||||||
|
Message: "Enter the matched ingress:",
|
||||||
|
Help: "Matching ingress resource object, the matching format is: namespace/ingress name",
|
||||||
|
}, &ing)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ing = strings.TrimSpace(ing)
|
||||||
|
if ing != "" {
|
||||||
|
ings = append(ings, ing)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = continueA.Ask()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !continueA.resp {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i.printer.Yesln(iconIdent + "Ingress:")
|
||||||
|
as, err := recursivePrompt(i.structName, i.schema, i.scope, i.printer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ok, ve := validate(i.vld, as); !ok {
|
||||||
|
i.printer.Noln(failedToValidate)
|
||||||
|
i.printer.Noln(ve)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
i.resp = &IngressMatchRule{
|
||||||
|
Ingress: ings,
|
||||||
|
Config: as,
|
||||||
|
}
|
||||||
|
i.printer.Yesln(addConfSuccessful)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DomainAsker struct {
|
||||||
|
resp *DomainMatchRule
|
||||||
|
|
||||||
|
structName string
|
||||||
|
schema *types.JSONSchemaProps
|
||||||
|
scope types.Scope
|
||||||
|
|
||||||
|
vld *jsonschema.Schema // for validation
|
||||||
|
printer *utils.YesOrNoPrinter
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDomainAsker(structName string, schema *types.JSONSchemaProps, vld *jsonschema.Schema, printer *utils.YesOrNoPrinter) *DomainAsker {
|
||||||
|
return &DomainAsker{
|
||||||
|
structName: structName,
|
||||||
|
schema: schema,
|
||||||
|
vld: vld,
|
||||||
|
printer: printer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DomainAsker) Ask() error {
|
||||||
|
continueA := newContinueAsker(d.printer)
|
||||||
|
doms := make([]string, 0)
|
||||||
|
for {
|
||||||
|
var dom string
|
||||||
|
err := utils.AskOne(&survey.Input{
|
||||||
|
Message: "Enter the matched domain:",
|
||||||
|
Help: "match domain name, support generic domain name",
|
||||||
|
}, &dom)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dom = strings.TrimSpace(dom)
|
||||||
|
if dom != "" {
|
||||||
|
doms = append(doms, dom)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = continueA.Ask()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !continueA.resp {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d.printer.Yesln(iconIdent + "Domain:")
|
||||||
|
as, err := recursivePrompt(d.structName, d.schema, d.scope, d.printer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ok, ve := validate(d.vld, as); !ok {
|
||||||
|
d.printer.Noln(failedToValidate)
|
||||||
|
d.printer.Noln(ve)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
d.resp = &DomainMatchRule{
|
||||||
|
Domain: doms,
|
||||||
|
Config: as,
|
||||||
|
}
|
||||||
|
d.printer.Yesln(addConfSuccessful)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type GlobalConfAsker struct {
|
||||||
|
resp map[string]interface{}
|
||||||
|
|
||||||
|
structName string
|
||||||
|
schema *types.JSONSchemaProps
|
||||||
|
scope types.Scope
|
||||||
|
|
||||||
|
vld *jsonschema.Schema // for validation
|
||||||
|
printer *utils.YesOrNoPrinter
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGlobalConfAsker(structName string, schema *types.JSONSchemaProps, vld *jsonschema.Schema, printer *utils.YesOrNoPrinter) *GlobalConfAsker {
|
||||||
|
return &GlobalConfAsker{
|
||||||
|
structName: structName,
|
||||||
|
schema: schema,
|
||||||
|
vld: vld,
|
||||||
|
printer: printer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GlobalConfAsker) Ask() error {
|
||||||
|
g.printer.Yesln(iconIdent + "Global:")
|
||||||
|
as, err := recursivePrompt(g.structName, g.schema, g.scope, g.printer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ok, ve := validate(g.vld, as); !ok {
|
||||||
|
g.printer.Noln(failedToValidate)
|
||||||
|
g.printer.Noln(ve)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
g.resp = as.(map[string]interface{})
|
||||||
|
g.printer.Yesln(addConfSuccessful)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type continueAsker struct {
|
||||||
|
resp bool
|
||||||
|
|
||||||
|
printer *utils.YesOrNoPrinter
|
||||||
|
}
|
||||||
|
|
||||||
|
func newContinueAsker(printer *utils.YesOrNoPrinter) *continueAsker {
|
||||||
|
return &continueAsker{printer: printer}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *continueAsker) Ask() error {
|
||||||
|
resp := true
|
||||||
|
err := utils.AskOne(&survey.Confirm{
|
||||||
|
Message: fmt.Sprintf("%scontinue?", c.printer.Ident()),
|
||||||
|
Default: true,
|
||||||
|
}, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.resp = resp
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type rewriteAsker struct {
|
||||||
|
resp bool
|
||||||
|
|
||||||
|
printer *utils.YesOrNoPrinter
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRewriteAsker(printer *utils.YesOrNoPrinter) *rewriteAsker {
|
||||||
|
return &rewriteAsker{printer: printer}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rewriteAsker) Ask() error {
|
||||||
|
resp := false
|
||||||
|
err := utils.AskOne(&survey.Confirm{
|
||||||
|
Message: fmt.Sprintf("%sThe configuration already exists as shown above. Do you want to rewrite it?", r.printer.Ident()),
|
||||||
|
Default: false,
|
||||||
|
}, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.resp = resp
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type scopeAsker struct {
|
||||||
|
resp types.Scope
|
||||||
|
|
||||||
|
printer *utils.YesOrNoPrinter
|
||||||
|
}
|
||||||
|
|
||||||
|
func newScopeAsker(printer *utils.YesOrNoPrinter) *scopeAsker {
|
||||||
|
return &scopeAsker{printer: printer}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scopeAsker) Ask() error {
|
||||||
|
var resp string
|
||||||
|
err := utils.AskOne(&survey.Select{
|
||||||
|
Message: fmt.Sprintf("%sChoose a configuration effective scope or complete:", s.printer.Ident()),
|
||||||
|
Options: []string{
|
||||||
|
// TODO(WeixinX): Not visible to the user, instead Global, Ingress, and Domain are asked in ruleAsker
|
||||||
|
string(types.ScopeInstance),
|
||||||
|
string(types.ScopeGlobal),
|
||||||
|
"Complete",
|
||||||
|
},
|
||||||
|
Default: string(types.ScopeInstance),
|
||||||
|
}, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.resp = types.Scope(resp)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ruleAsker struct {
|
||||||
|
resp Rule
|
||||||
|
|
||||||
|
printer *utils.YesOrNoPrinter
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRuleAsker(printer *utils.YesOrNoPrinter) *ruleAsker {
|
||||||
|
return &ruleAsker{printer: printer}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ruleAsker) Ask() error {
|
||||||
|
var resp string
|
||||||
|
err := utils.AskOne(&survey.Select{
|
||||||
|
Message: fmt.Sprintf("%sChoose Ingress or Domain:", r.printer.Ident()),
|
||||||
|
Options: []string{
|
||||||
|
string(ruleIngress),
|
||||||
|
string(ruleDomain),
|
||||||
|
},
|
||||||
|
Default: string(ruleIngress),
|
||||||
|
}, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.resp = Rule(resp)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type WasmPluginSpecConf struct {
|
||||||
|
DefaultConfig map[string]interface{} `yaml:"defaultConfig,omitempty"`
|
||||||
|
MatchRules []MatchRule `yaml:"matchRules,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPluginSpecConf() *WasmPluginSpecConf {
|
||||||
|
return &WasmPluginSpecConf{
|
||||||
|
MatchRules: make([]MatchRule, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *WasmPluginSpecConf) String() string {
|
||||||
|
if len(p.DefaultConfig) == 0 && len(p.MatchRules) == 0 {
|
||||||
|
return " "
|
||||||
|
}
|
||||||
|
|
||||||
|
b, _ := utils.MarshalYamlWithIndent(p, 2)
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MatchRule interface {
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type IngressMatchRule struct {
|
||||||
|
Ingress []string `json:"ingress" yaml:"ingress" mapstructure:"ingress"`
|
||||||
|
Config interface{} `json:"config" yaml:"config" mapstructure:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i IngressMatchRule) String() string {
|
||||||
|
b, _ := utils.MarshalYamlWithIndent(i, 2)
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeIngressMatchRule(obj map[string]interface{}) (*IngressMatchRule, error) {
|
||||||
|
var ing IngressMatchRule
|
||||||
|
if err := mapstructure.Decode(obj, &ing); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ing, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DomainMatchRule struct {
|
||||||
|
Domain []string `json:"domain" yaml:"domain" mapstructure:"domain"`
|
||||||
|
Config interface{} `json:"config" yaml:"config" mapstructure:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DomainMatchRule) String() string {
|
||||||
|
b, _ := utils.MarshalYamlWithIndent(d, 2)
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeDomainMatchRule(obj map[string]interface{}) (*DomainMatchRule, error) {
|
||||||
|
var dom DomainMatchRule
|
||||||
|
if err := mapstructure.Decode(obj, &dom); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &dom, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Rule string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ruleIngress Rule = "Ingress"
|
||||||
|
ruleDomain Rule = "Domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
func recursivePrompt(structName string, schema *types.JSONSchemaProps, selScope types.Scope, printer *utils.YesOrNoPrinter) (interface{}, error) {
|
||||||
|
printer.IncIdentRepeat()
|
||||||
|
defer printer.DecIndentRepeat()
|
||||||
|
return doPrompt(structName, nil, schema, types.ScopeAll, selScope, printer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func doPrompt(fieldName string, parent, schema *types.JSONSchemaProps, oriScope, selScope types.Scope, printer *utils.YesOrNoPrinter) (interface{}, error) {
|
||||||
|
if schema.Title == "" {
|
||||||
|
schema.Title = fieldName
|
||||||
|
}
|
||||||
|
if schema.Description == "" {
|
||||||
|
schema.Description = fieldName
|
||||||
|
}
|
||||||
|
required := true
|
||||||
|
if parent != nil {
|
||||||
|
required = isRequired(fieldName, parent.Required)
|
||||||
|
}
|
||||||
|
msg, help := fieldTips(fieldName, parent, schema, required, printer)
|
||||||
|
|
||||||
|
switch types.JsonType(schema.Type) {
|
||||||
|
case types.JsonTypeObject:
|
||||||
|
printer.Println(iconIdent + msg)
|
||||||
|
obj := make(map[string]interface{})
|
||||||
|
m := schema.GetPropertiesOrderMap()
|
||||||
|
for _, name := range m.Keys() {
|
||||||
|
propI, _ := m.Get(name)
|
||||||
|
prop := propI.(types.JSONSchemaProps)
|
||||||
|
|
||||||
|
if parent == nil { // keep topmost scope
|
||||||
|
if prop.Scope == types.ScopeGlobal {
|
||||||
|
oriScope = types.ScopeGlobal
|
||||||
|
} else if prop.Scope == types.ScopeInstance || prop.Scope == "" {
|
||||||
|
oriScope = types.ScopeInstance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !matchesScope(oriScope, selScope, prop.Scope) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
printer.IncIdentRepeat()
|
||||||
|
v, err := doPrompt(name, schema, &prop, oriScope, selScope, printer)
|
||||||
|
printer.DecIndentRepeat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if v != nil {
|
||||||
|
obj[name] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(obj) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return obj, nil
|
||||||
|
|
||||||
|
case types.JsonTypeArray:
|
||||||
|
printer.Println(iconIdent + msg)
|
||||||
|
continueA := newContinueAsker(printer)
|
||||||
|
arr := make([]interface{}, 0)
|
||||||
|
for {
|
||||||
|
printer.IncIdentRepeat()
|
||||||
|
v, err := doPrompt("item", schema, schema.Items.Schema, oriScope, selScope, printer)
|
||||||
|
if err != nil {
|
||||||
|
printer.DecIndentRepeat()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if v != nil {
|
||||||
|
arr = append(arr, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = continueA.Ask()
|
||||||
|
printer.DecIndentRepeat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !continueA.resp {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(arr) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return arr, nil
|
||||||
|
|
||||||
|
case types.JsonTypeInteger, types.JsonTypeNumber, types.JsonTypeBoolean, types.JsonTypeString:
|
||||||
|
for {
|
||||||
|
var inp string
|
||||||
|
if err := utils.AskOne(&survey.Input{
|
||||||
|
Message: msg,
|
||||||
|
Help: help,
|
||||||
|
}, &inp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if inp == "" && !required {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch types.JsonType(schema.Type) {
|
||||||
|
case types.JsonTypeInteger:
|
||||||
|
v, err := strconv.ParseInt(inp, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, strconv.ErrSyntax) {
|
||||||
|
printer.Nof("%s %q type is invalid.\n", invalidSyntax, inp)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
case types.JsonTypeNumber:
|
||||||
|
v, err := strconv.ParseFloat(inp, 64)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, strconv.ErrSyntax) {
|
||||||
|
printer.Nof("%s %q type is invalid.\n", invalidSyntax, inp)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
case types.JsonTypeBoolean:
|
||||||
|
v, err := strconv.ParseBool(inp)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, strconv.ErrSyntax) {
|
||||||
|
printer.Nof("%s %q type is invalid.\n", invalidSyntax, inp)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
case types.JsonTypeString:
|
||||||
|
return inp, nil
|
||||||
|
default:
|
||||||
|
return inp, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported type: %s", schema.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchesScope(oriScope, selScope, scope types.Scope) bool {
|
||||||
|
return (oriScope == selScope) ||
|
||||||
|
(selScope == types.ScopeInstance && (scope == selScope || scope == "" || scope == types.ScopeAll)) ||
|
||||||
|
(selScope == types.ScopeGlobal && (scope == selScope || scope == types.ScopeAll))
|
||||||
|
}
|
||||||
|
|
||||||
|
func fieldTips(fieldName string, parent, schema *types.JSONSchemaProps, required bool, printer *utils.YesOrNoPrinter) (string, string) {
|
||||||
|
var msg, help string
|
||||||
|
if fieldName == "item" {
|
||||||
|
msg = fmt.Sprintf("%s%s(%s)", printer.Ident(), fieldName, schema.Type)
|
||||||
|
help = fmt.Sprintf("%s%s: %s", printer.Ident(), parent.Title, parent.Description)
|
||||||
|
} else {
|
||||||
|
req := schema.JoinRequirementsBy(types.I18nEN_US, required)
|
||||||
|
msg = fmt.Sprintf("%s%s(%s, %s)", printer.Ident(), fieldName, schema.Type, req)
|
||||||
|
help = fmt.Sprintf("%s%s: %s", printer.Ident(), schema.Title, schema.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg, help
|
||||||
|
}
|
||||||
|
|
||||||
|
func isRequired(name string, required []string) bool {
|
||||||
|
req := false
|
||||||
|
for _, n := range required {
|
||||||
|
if name == n {
|
||||||
|
req = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
func validate(schema *jsonschema.Schema, v interface{}) (bool, error) {
|
||||||
|
if err := schema.Validate(v); err != nil {
|
||||||
|
err = convertValidationError(err.(*jsonschema.ValidationError))
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertValidationError(ve *jsonschema.ValidationError) error {
|
||||||
|
de := ve.DetailedOutput()
|
||||||
|
if de.Valid {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
errs := make([]error, 0)
|
||||||
|
if de.Error != "" {
|
||||||
|
errs = append(errs, errors.New(de.Error))
|
||||||
|
}
|
||||||
|
errs = append(errs, doConvertValidationError(de.Errors, errs)...)
|
||||||
|
if len(errs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret error
|
||||||
|
for i, err := range errs {
|
||||||
|
if i == 0 {
|
||||||
|
ret = fmt.Errorf("%w", err)
|
||||||
|
} else {
|
||||||
|
ret = fmt.Errorf("%s\n%w", ret.Error(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func doConvertValidationError(de []jsonschema.Detailed, errs []error) []error {
|
||||||
|
for _, e := range de {
|
||||||
|
if e.Error != "" {
|
||||||
|
errs = append(errs, errors.New(e.Error))
|
||||||
|
}
|
||||||
|
if len(e.Errors) > 0 {
|
||||||
|
errs = append(errs, doConvertValidationError(e.Errors, errs)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
383
pkg/cmd/hgctl/plugin/install/install.go
Normal file
383
pkg/cmd/hgctl/plugin/install/install.go
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package install
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
k8s "github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/build"
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/config"
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/option"
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/types"
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/utils"
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/options"
|
||||||
|
|
||||||
|
"github.com/AlecAivazis/survey/v2/terminal"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/santhosh-tekuri/jsonschema/v5"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
k8serr "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type installer struct {
|
||||||
|
optionFile string
|
||||||
|
bldOpts option.BuildOptions
|
||||||
|
insOpts option.InstallOptions
|
||||||
|
|
||||||
|
cli *k8s.WasmPluginClient
|
||||||
|
w io.Writer
|
||||||
|
utils.Debugger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCommand() *cobra.Command {
|
||||||
|
var ins installer
|
||||||
|
v := viper.New()
|
||||||
|
|
||||||
|
installCmd := &cobra.Command{
|
||||||
|
Use: "install",
|
||||||
|
Aliases: []string{"ins", "i"},
|
||||||
|
Short: "Install WASM plugin",
|
||||||
|
Example: ` # Install WASM plugin using a WasmPlugin manifest
|
||||||
|
hgctl plugin install -y plugin-conf.yaml
|
||||||
|
|
||||||
|
# Install WASM plugin through the Golang WASM plugin project (do it by relying on option.yaml now)
|
||||||
|
docker login
|
||||||
|
hgctl plugin install -g ./
|
||||||
|
`,
|
||||||
|
|
||||||
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
cmdutil.CheckErr(ins.config(v, cmd))
|
||||||
|
},
|
||||||
|
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
cmdutil.CheckErr(ins.install(cmd.PersistentFlags()))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := installCmd.PersistentFlags()
|
||||||
|
options.AddKubeConfigFlags(flags)
|
||||||
|
option.AddOptionFileFlag(&ins.optionFile, flags)
|
||||||
|
v.BindPFlags(flags)
|
||||||
|
|
||||||
|
flags.StringP("namespace", "n", k8s.HigressNamespace, "Namespace where Higress was installed")
|
||||||
|
v.BindPFlag("install.namespace", flags.Lookup("namespace"))
|
||||||
|
v.SetDefault("install.namespace", k8s.DefaultHigressNamespace)
|
||||||
|
|
||||||
|
flags.StringP("spec-yaml", "s", "./out/spec.yaml", "Use to validate WASM plugin configuration")
|
||||||
|
v.BindPFlag("install.spec-yaml", flags.Lookup("spec-yaml"))
|
||||||
|
v.SetDefault("install.spec-yaml", "./test/plugin-spec-yaml")
|
||||||
|
|
||||||
|
// TODO(WeixinX):
|
||||||
|
// - Change "--from-yaml (-y)" to "--from-oci (-o)" and implement command line interaction like "--from-go-src"
|
||||||
|
// - Add "--from-jar (-j)"
|
||||||
|
flags.StringP("from-yaml", "y", "./test/plugin-conf.yaml", "Install WASM plugin using a WasmPlugin manifest")
|
||||||
|
v.BindPFlag("install.from-yaml", flags.Lookup("from-yaml"))
|
||||||
|
v.SetDefault("install.from-yaml", "./test/plugin-conf.yaml")
|
||||||
|
|
||||||
|
flags.StringP("from-go-src", "g", "", "Install WASM plugin through the Golang WASM plugin project")
|
||||||
|
v.BindPFlag("install.from-go-src", flags.Lookup("from-go-src"))
|
||||||
|
v.SetDefault("install.from-go-src", "")
|
||||||
|
|
||||||
|
flags.BoolP("debug", "", false, "Enable debug mode")
|
||||||
|
v.BindPFlag("install.debug", flags.Lookup("debug"))
|
||||||
|
v.SetDefault("install.debug", false)
|
||||||
|
|
||||||
|
return installCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ins *installer) config(v *viper.Viper, cmd *cobra.Command) error {
|
||||||
|
allOpt, err := option.ParseOptions(ins.optionFile, v, cmd.PersistentFlags())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// TODO(WeixinX): Avoid relying on build options, add a new option "--push/--image" for installing from go src
|
||||||
|
ins.bldOpts = allOpt.Build
|
||||||
|
ins.insOpts = allOpt.Install
|
||||||
|
|
||||||
|
dynCli, err := k8s.NewDynamicClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to build kubernetes dynamic client")
|
||||||
|
}
|
||||||
|
ins.cli = k8s.NewWasmPluginClient(dynCli)
|
||||||
|
ins.w = cmd.OutOrStdout()
|
||||||
|
ins.Debugger = utils.NewDefaultDebugger(ins.insOpts.Debug, ins.w)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ins *installer) install(flags *pflag.FlagSet) (err error) {
|
||||||
|
ins.Debugf("install option:\n%s\n", ins.String())
|
||||||
|
|
||||||
|
if ins.insOpts.FromGoSrc == "" || flags.Changed("from-yaml") {
|
||||||
|
err = ins.yamlHandler()
|
||||||
|
} else {
|
||||||
|
err = ins.goHandler()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ins *installer) yamlHandler() error {
|
||||||
|
return ins.doInstall(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ins *installer) goHandler() error {
|
||||||
|
// 0. ensure output.type == image
|
||||||
|
if ins.bldOpts.Output.Type != "image" {
|
||||||
|
return errors.New("output type must be image")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. build the WASM plugin project and push the image to the registry
|
||||||
|
bld, err := build.NewBuilder(func(b *build.Builder) error {
|
||||||
|
b.BuildOptions = ins.bldOpts
|
||||||
|
b.Debug = ins.insOpts.Debug
|
||||||
|
b.WithManualClean() // keep spec.yaml
|
||||||
|
b.WithWriter(ins.w)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to initialize builder")
|
||||||
|
}
|
||||||
|
err = bld.Build()
|
||||||
|
if err != nil {
|
||||||
|
bld.Debugln("clean up for error ...")
|
||||||
|
bld.CleanupForError()
|
||||||
|
return errors.Wrap(err, "failed to build and push wasm plugin")
|
||||||
|
}
|
||||||
|
defer bld.Cleanup()
|
||||||
|
|
||||||
|
// 2. command-line interaction lets the user enter the wasm plugin configuration
|
||||||
|
specPath := bld.SpecYAMLPath()
|
||||||
|
spec, err := types.ParseSpecYAML(specPath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to parse spec.yaml: %s", specPath)
|
||||||
|
}
|
||||||
|
vld, err := buildSchemaValidator(spec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
example := spec.GetConfigExample()
|
||||||
|
schema := spec.Spec.ConfigSchema.OpenAPIV3Schema
|
||||||
|
printer := utils.DefaultPrinter()
|
||||||
|
asker := NewWasmPluginSpecConfAsker(
|
||||||
|
NewIngressAsker(bld.Model, schema, vld, printer),
|
||||||
|
NewDomainAsker(bld.Model, schema, vld, printer),
|
||||||
|
NewGlobalConfAsker(bld.Model, schema, vld, printer),
|
||||||
|
printer,
|
||||||
|
)
|
||||||
|
|
||||||
|
printer.Yesln("Please enter the configurations for the WASM plugin you want to install:")
|
||||||
|
printer.Yesln("Configuration example:")
|
||||||
|
printer.Yesf("\n%s\n", example)
|
||||||
|
|
||||||
|
err = asker.Ask()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, terminal.InterruptErr) {
|
||||||
|
printer.Noln(askInterrupted)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. generate the WasmPlugin manifest
|
||||||
|
wpc := asker.resp
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to marshal wasm plugin config")
|
||||||
|
}
|
||||||
|
// get the parameters of plugin-conf.yaml from spec.yaml
|
||||||
|
pc, err := config.ExtractPluginConfFrom(spec, wpc.String(), bld.Output.Dest)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to get the parameters of plugin-conf.yaml from %s", specPath)
|
||||||
|
}
|
||||||
|
ins.Debugf("plugin-conf.yaml params:\n%s\n", pc.String())
|
||||||
|
if err = config.GenPluginConfYAML(pc, bld.TempDir()); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to generate plugin-conf.yaml")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. install by the manifest
|
||||||
|
ins.insOpts.FromYaml = bld.TempDir() + "/plugin-conf.yaml"
|
||||||
|
if err = ins.doInstall(false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ins *installer) doInstall(validate bool) error {
|
||||||
|
f, err := os.Open(ins.insOpts.FromYaml)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// multiple WASM plugins are separated by '---' in yaml, but we only handle first one
|
||||||
|
// TODO(WeixinX): Use WasmPlugin Object type instead of Unstructured
|
||||||
|
obj := &unstructured.Unstructured{}
|
||||||
|
dc := k8syaml.NewYAMLOrJSONDecoder(f, 4096)
|
||||||
|
if err = dc.Decode(obj); err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to parse wasm plugin from manifest %q", ins.insOpts.FromYaml)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValidAPIVersion(obj) {
|
||||||
|
fmt.Fprintf(ins.w, "Warning: wasm plugin %q has invalid apiVersion, automatically modified: %q -> %q\n",
|
||||||
|
obj.GetName(), obj.GetAPIVersion(), k8s.HigressExtAPIVersion)
|
||||||
|
obj.SetAPIVersion(k8s.HigressExtAPIVersion)
|
||||||
|
}
|
||||||
|
if !isValidKind(obj) {
|
||||||
|
fmt.Fprintf(ins.w, "Warning: wasm plugin %q has invalid kind, automatically modified: %q -> %q\n",
|
||||||
|
obj.GetName(), obj.GetKind(), k8s.WasmPluginKind)
|
||||||
|
obj.SetKind(k8s.WasmPluginKind)
|
||||||
|
}
|
||||||
|
if !isValidNamespace(obj) {
|
||||||
|
fmt.Fprintf(ins.w, "Warning: wasm plugin %q has invalid namespace, automatically modified: %q -> %q\n",
|
||||||
|
obj.GetName(), obj.GetNamespace(), k8s.HigressNamespace)
|
||||||
|
obj.SetNamespace(k8s.HigressNamespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate wasm plugin config
|
||||||
|
if validate {
|
||||||
|
if wps, ok := obj.Object["spec"].(map[string]interface{}); ok {
|
||||||
|
if err = ins.validateWasmPluginConfig(wps); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return errors.New("failed to get the spec filed of wasm plugin")
|
||||||
|
}
|
||||||
|
ins.Debugln("successfully validated wasm plugin config")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ins.cli.Create(context.TODO(), obj)
|
||||||
|
if err != nil {
|
||||||
|
if k8serr.IsAlreadyExists(err) {
|
||||||
|
fmt.Fprintf(ins.w, "wasm plugin %q already exists\n",
|
||||||
|
fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName()))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.Wrapf(err, "failed to install wasm plugin %q",
|
||||||
|
fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(ins.w, "Installed wasm plugin %q\n", fmt.Sprintf("%s/%s", result.GetNamespace(), result.GetName()))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidAPIVersion(obj *unstructured.Unstructured) bool {
|
||||||
|
return obj.GetAPIVersion() == k8s.HigressExtAPIVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidKind(obj *unstructured.Unstructured) bool {
|
||||||
|
return obj.GetKind() == k8s.WasmPluginKind
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidNamespace(obj *unstructured.Unstructured) bool {
|
||||||
|
return obj.GetNamespace() == k8s.HigressNamespace
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ins *installer) validateWasmPluginConfig(wps map[string]interface{}) error {
|
||||||
|
spec, err := types.ParseSpecYAML(ins.insOpts.SpecYaml)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to parse %s", ins.insOpts.SpecYaml)
|
||||||
|
}
|
||||||
|
vld, err := buildSchemaValidator(spec)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to build schema validator")
|
||||||
|
}
|
||||||
|
|
||||||
|
if dc, ok := wps["defaultConfig"].(map[string]interface{}); ok {
|
||||||
|
if ok, err = validate(vld, dc); !ok {
|
||||||
|
return errors.Wrap(err, "failed to validate default config")
|
||||||
|
}
|
||||||
|
|
||||||
|
// debug
|
||||||
|
b, _ := utils.MarshalYamlWithIndent(dc, 2)
|
||||||
|
ins.Debugf("default config:\n%s\n", string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
if mrs, ok := wps["matchRules"].([]interface{}); ok {
|
||||||
|
for _, mr := range mrs {
|
||||||
|
if r, ok := mr.(map[string]interface{}); ok {
|
||||||
|
if _, ok = r["ingress"]; ok {
|
||||||
|
ing, err := decodeIngressMatchRule(r)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to parse ingress match rule")
|
||||||
|
}
|
||||||
|
if ok, err = validate(vld, ing.Config); !ok {
|
||||||
|
return errors.Wrap(err, "failed to validate ingress match rule")
|
||||||
|
}
|
||||||
|
|
||||||
|
ins.Debugf("ingress match rule:\n%s\n", ing.String())
|
||||||
|
|
||||||
|
} else if _, ok = r["domain"]; ok {
|
||||||
|
dom, err := decodeDomainMatchRule(r)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to parse domain match rule")
|
||||||
|
}
|
||||||
|
if ok, err = validate(vld, dom.Config); !ok {
|
||||||
|
return errors.Wrap(err, "failed to validate ingress match rule")
|
||||||
|
}
|
||||||
|
|
||||||
|
ins.Debugf("domain match rule:\n%s\n", dom.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSchemaValidator(spec *types.WasmPluginMeta) (*jsonschema.Schema, error) {
|
||||||
|
if spec == nil {
|
||||||
|
return nil, errors.New("spec is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
schema := spec.Spec.ConfigSchema.OpenAPIV3Schema
|
||||||
|
if schema == nil {
|
||||||
|
return nil, errors.New("spec has no config schema")
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := json.Marshal(schema)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c := jsonschema.NewCompiler()
|
||||||
|
c.Draft = jsonschema.Draft4
|
||||||
|
err = c.AddResource("schema.json", strings.NewReader(string(b)))
|
||||||
|
vld, err := c.Compile("schema.json")
|
||||||
|
if err != nil {
|
||||||
|
errors.Wrap(err, "failed to compile schema")
|
||||||
|
}
|
||||||
|
|
||||||
|
return vld, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ins *installer) String() string {
|
||||||
|
b, err := json.MarshalIndent(ins.insOpts, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("OptionFile: %s\n%s", ins.optionFile, string(b))
|
||||||
|
}
|
||||||
78
pkg/cmd/hgctl/plugin/ls/ls.go
Normal file
78
pkg/cmd/hgctl/plugin/ls/ls.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
k8s "github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/options"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"k8s.io/apimachinery/pkg/util/duration"
|
||||||
|
"k8s.io/cli-runtime/pkg/printers"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewCommand() *cobra.Command {
|
||||||
|
lsCmd := &cobra.Command{
|
||||||
|
Use: "ls",
|
||||||
|
Aliases: []string{"l"},
|
||||||
|
Short: "List all installed WASM plugins",
|
||||||
|
Example: ` hgctl plugin ls`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
cmdutil.CheckErr(runLs(cmd.OutOrStdout()))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := lsCmd.PersistentFlags()
|
||||||
|
options.AddKubeConfigFlags(flags)
|
||||||
|
k8s.AddHigressNamespaceFlags(flags)
|
||||||
|
|
||||||
|
return lsCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLs(w io.Writer) error {
|
||||||
|
dynCli, err := k8s.NewDynamicClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to build kubernetes client")
|
||||||
|
}
|
||||||
|
cli := k8s.NewWasmPluginClient(dynCli)
|
||||||
|
|
||||||
|
list, err := cli.List(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to list all wasm plugins")
|
||||||
|
}
|
||||||
|
|
||||||
|
printer := printers.GetNewTabWriter(w)
|
||||||
|
now := time.Now()
|
||||||
|
fmt.Fprintf(printer, "NAME\tAGE\n")
|
||||||
|
for _, item := range list.Items {
|
||||||
|
fmt.Fprintf(printer, "%s\t%s\n", item.GetName(), getAge(now, item.GetCreationTimestamp().Time))
|
||||||
|
}
|
||||||
|
if err = printer.Flush(); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to flush output")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAge(now time.Time, create time.Time) string {
|
||||||
|
return duration.ShortHumanDuration(now.Sub(create))
|
||||||
|
}
|
||||||
96
pkg/cmd/hgctl/plugin/option/option.go
Normal file
96
pkg/cmd/hgctl/plugin/option/option.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package option
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Option struct {
|
||||||
|
Version string `json:"version" yaml:"version" mapstructure:"version"`
|
||||||
|
Build BuildOptions `json:"build" yaml:"build" mapstructure:"build"`
|
||||||
|
Test TestOptions `json:"test" yaml:"test" mapstructure:"test"`
|
||||||
|
Install InstallOptions `json:"install" yaml:"install" mapstructure:"install"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BuildOptions struct {
|
||||||
|
Builder BuilderVersion `json:"builder" yaml:"builder" mapstructure:"builder"`
|
||||||
|
Input string `json:"input" yaml:"input" mapstructure:"input"`
|
||||||
|
Output Output `json:"output" yaml:"output" mapstructure:"output"`
|
||||||
|
DockerAuth string `json:"docker-auth" yaml:"docker-auth" mapstructure:"docker-auth"`
|
||||||
|
ModelDir string `json:"model-dir" yaml:"model-dir" mapstructure:"model-dir"`
|
||||||
|
Model string `json:"model" yaml:"model" mapstructure:"model"`
|
||||||
|
Debug bool `json:"debug" yaml:"debug" mapstructure:"debug"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestOptions struct {
|
||||||
|
Name string `json:"name" yaml:"name" mapstructure:"name"`
|
||||||
|
FromPath string `json:"from-path" yaml:"from-path" mapstructure:"from-path"`
|
||||||
|
TestPath string `json:"test-path" yaml:"test-path" mapstructure:"test-path"`
|
||||||
|
ComposeFile string `json:"compose-file" yaml:"compose-file" mapstructure:"compose-file"`
|
||||||
|
Detach bool `json:"detach" yaml:"detach" mapstructure:"detach"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InstallOptions struct {
|
||||||
|
Namespace string `json:"namespace" yaml:"namespace" mapstructure:"namespace"`
|
||||||
|
SpecYaml string `json:"spec-yaml" yaml:"spec-yaml" mapstructure:"spec-yaml"`
|
||||||
|
FromYaml string `json:"from-yaml" yaml:"from-yaml" mapstructure:"from-yaml"`
|
||||||
|
FromGoSrc string `json:"from-go-src" yaml:"from-go-src" mapstructure:"from-go-src"`
|
||||||
|
Debug bool `json:"debug" yaml:"debug" mapstructure:"debug"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BuilderVersion struct {
|
||||||
|
Go string `json:"go" yaml:"go" mpastructure:"go"`
|
||||||
|
TinyGo string `json:"tinygo" yaml:"tinygo" mapstructure:"tinygo"`
|
||||||
|
Oras string `json:"oras" yaml:"oras" mapstructure:"oras"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Output struct {
|
||||||
|
Type string `json:"type" yaml:"type" mapstructure:"type"`
|
||||||
|
Dest string `json:"dest" yaml:"dest" mapstructure:"dest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseOptions reads `option.yaml` and parses it into Option struct
|
||||||
|
func ParseOptions(optionFile string, v *viper.Viper, flags *pflag.FlagSet) (*Option, error) {
|
||||||
|
_, err := os.Stat(optionFile)
|
||||||
|
if err != nil {
|
||||||
|
// `option-file` is explicitly specified, but the given file does not exist
|
||||||
|
if errors.Is(err, os.ErrNotExist) && flags.Changed("option-file") {
|
||||||
|
return nil, errors.Errorf("option file does not exist: %q", optionFile)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
v.SetConfigFile(optionFile)
|
||||||
|
if err = v.ReadInConfig(); err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to read option file %q", optionFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var opt Option
|
||||||
|
if err = v.Unmarshal(&opt); err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to unmarshal option file %q", optionFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &opt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddOptionFileFlag adds `option-file` flag
|
||||||
|
func AddOptionFileFlag(optionFile *string, flags *pflag.FlagSet) {
|
||||||
|
flags.StringVarP(optionFile, "option-file", "f", "./option.yaml",
|
||||||
|
"Option file for build, test and install")
|
||||||
|
}
|
||||||
89
pkg/cmd/hgctl/plugin/option/template.go
Normal file
89
pkg/cmd/hgctl/plugin/option/template.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package option
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
const optionYAML = `# File generated by hgctl. Modify as required.
|
||||||
|
|
||||||
|
version: 1.0.0
|
||||||
|
|
||||||
|
build:
|
||||||
|
# The official builder image version
|
||||||
|
builder:
|
||||||
|
go: 1.19
|
||||||
|
tinygo: 0.28.1
|
||||||
|
oras: 1.0.0
|
||||||
|
# The WASM plugin project directory
|
||||||
|
input: ./
|
||||||
|
# The output of the build products
|
||||||
|
output:
|
||||||
|
# Choose between 'files' and 'image'
|
||||||
|
type: files
|
||||||
|
# Destination address: when type=files, specify the local directory path, e.g., './out' or
|
||||||
|
# type=image, specify the remote docker repository, e.g., 'docker.io/<your_username>/<your_image>'
|
||||||
|
dest: ./out
|
||||||
|
# The authentication configuration for pushing image to the docker repository
|
||||||
|
docker-auth: ~/.docker/config.json
|
||||||
|
# The directory for the WASM plugin configuration structure
|
||||||
|
model-dir: ./
|
||||||
|
# The WASM plugin configuration structure name
|
||||||
|
model: PluginConfig
|
||||||
|
# Enable debug mode
|
||||||
|
debug: false
|
||||||
|
|
||||||
|
test:
|
||||||
|
# Test environment name, that is a docker compose project name
|
||||||
|
name: wasm-test
|
||||||
|
# The output path to build products, that is the source of test configuration parameters
|
||||||
|
from-path: ./out
|
||||||
|
# The test configuration source
|
||||||
|
test-path: ./test
|
||||||
|
# Docker compose configuration, which is empty, looks for the following files from 'test-path':
|
||||||
|
# compose.yaml, compose.yml, docker-compose.yml, docker-compose.yaml
|
||||||
|
compose-file:
|
||||||
|
# Detached mode: Run containers in the background
|
||||||
|
detach: false
|
||||||
|
|
||||||
|
install:
|
||||||
|
# The namespace of the installation
|
||||||
|
namespace: higress-system
|
||||||
|
# Use to validate WASM plugin configuration when install by yaml
|
||||||
|
spec-yaml: ./out/spec.yaml
|
||||||
|
# Installation source. Choose between 'from-yaml' and 'from-go-project'
|
||||||
|
from-yaml: ./test/plugin-conf.yaml
|
||||||
|
# If 'from-go-src' is non-empty, the output type of the build option must be 'image'
|
||||||
|
from-go-src:
|
||||||
|
# Enable debug mode
|
||||||
|
debug: false
|
||||||
|
`
|
||||||
|
|
||||||
|
func GenOptionYAML(dir string) error {
|
||||||
|
path := fmt.Sprintf("%s/option.yaml", dir)
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if _, err = f.WriteString(optionYAML); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
45
pkg/cmd/hgctl/plugin/plugin.go
Normal file
45
pkg/cmd/hgctl/plugin/plugin.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/build"
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/config"
|
||||||
|
plugininit "github.com/alibaba/higress/pkg/cmd/hgctl/plugin/init"
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/install"
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/ls"
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/test"
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/uninstall"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewCommand() *cobra.Command {
|
||||||
|
pluginCommand := &cobra.Command{
|
||||||
|
Use: "plugin",
|
||||||
|
Aliases: []string{"plg", "p"},
|
||||||
|
Short: "For the Golang WASM plugin",
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginCommand.AddCommand(build.NewCommand())
|
||||||
|
pluginCommand.AddCommand(install.NewCommand())
|
||||||
|
pluginCommand.AddCommand(uninstall.NewCommand())
|
||||||
|
pluginCommand.AddCommand(ls.NewCommand())
|
||||||
|
pluginCommand.AddCommand(test.NewCommand())
|
||||||
|
pluginCommand.AddCommand(config.NewCommand())
|
||||||
|
pluginCommand.AddCommand(plugininit.NewCommand())
|
||||||
|
|
||||||
|
return pluginCommand
|
||||||
|
}
|
||||||
108
pkg/cmd/hgctl/plugin/test/clean.go
Normal file
108
pkg/cmd/hgctl/plugin/test/clean.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/docker"
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/option"
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/utils"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cleaner struct {
|
||||||
|
optionFile string
|
||||||
|
option.TestOptions
|
||||||
|
|
||||||
|
w io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCleanCommand() *cobra.Command {
|
||||||
|
var c cleaner
|
||||||
|
v := viper.New()
|
||||||
|
|
||||||
|
cleanCmd := &cobra.Command{
|
||||||
|
Use: "clean",
|
||||||
|
Aliases: []string{"cl"},
|
||||||
|
Short: "Clean the test environment, that is remove the source of test configuration",
|
||||||
|
Example: ` hgctl plugin test clean`,
|
||||||
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
cmdutil.CheckErr(c.config(v, cmd))
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
cmdutil.CheckErr(c.clean())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := cleanCmd.PersistentFlags()
|
||||||
|
option.AddOptionFileFlag(&c.optionFile, flags)
|
||||||
|
v.BindPFlags(flags)
|
||||||
|
|
||||||
|
flags.StringP("name", "p", "wasm-test", "Test environment name")
|
||||||
|
v.BindPFlag("test.name", flags.Lookup("name"))
|
||||||
|
v.SetDefault("test.name", "wasm-test")
|
||||||
|
|
||||||
|
// TODO(WeixinX): Obtain the test configuration source directory based on the test environment name (hgctl plugin test ls)
|
||||||
|
flags.StringP("test-path", "t", "./test", "Test configuration source")
|
||||||
|
v.BindPFlag("test.test-path", flags.Lookup("test-path"))
|
||||||
|
v.SetDefault("test.test-path", "./test")
|
||||||
|
|
||||||
|
return cleanCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cleaner) config(v *viper.Viper, cmd *cobra.Command) error {
|
||||||
|
allOpt, err := option.ParseOptions(c.optionFile, v, cmd.PersistentFlags())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.TestOptions = allOpt.Test
|
||||||
|
|
||||||
|
c.w = cmd.OutOrStdout()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cleaner) clean() error {
|
||||||
|
cli, err := docker.NewCompose(c.w)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to build the docker compose client")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = cli.Down(context.TODO(), c.Name)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to stop the test environment %q", c.Name)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(c.w, "Stopped the test environment %q\n", c.Name)
|
||||||
|
|
||||||
|
source, err := utils.GetAbsolutePath(c.TestPath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "invalid test configuration source %q", c.TestPath)
|
||||||
|
}
|
||||||
|
err = os.RemoveAll(source)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to remove the test configuration source %q", source)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(c.w, "Removed the source %q\n", source)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
175
pkg/cmd/hgctl/plugin/test/create.go
Normal file
175
pkg/cmd/hgctl/plugin/test/create.go
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/config"
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/option"
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/types"
|
||||||
|
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/utils"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type creator struct {
|
||||||
|
optionFile string
|
||||||
|
option.TestOptions
|
||||||
|
|
||||||
|
w io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCreateCommand() *cobra.Command {
|
||||||
|
var c creator
|
||||||
|
v := viper.New()
|
||||||
|
|
||||||
|
createCmd := &cobra.Command{
|
||||||
|
Use: "create",
|
||||||
|
Aliases: []string{"c"},
|
||||||
|
Short: "Create the test environment",
|
||||||
|
Example: ` # If the option.yaml file exists in the current path, do the following:
|
||||||
|
hgctl plugin test create
|
||||||
|
|
||||||
|
# Explicitly specify the source of the parameters (directory of the build
|
||||||
|
products) and the directory where the test configuration files is stored
|
||||||
|
hgctl plugin test create -d ./out -t ./test
|
||||||
|
`,
|
||||||
|
|
||||||
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
cmdutil.CheckErr(c.config(v, cmd))
|
||||||
|
},
|
||||||
|
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
cmdutil.CheckErr(c.create())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := createCmd.PersistentFlags()
|
||||||
|
option.AddOptionFileFlag(&c.optionFile, flags)
|
||||||
|
v.BindPFlags(flags)
|
||||||
|
|
||||||
|
flags.StringP("from-path", "d", "./out", "Path of storing the build products")
|
||||||
|
v.BindPFlag("test.from-path", flags.Lookup("from-path"))
|
||||||
|
v.SetDefault("test.from-path", "./out")
|
||||||
|
|
||||||
|
flags.StringP("test-path", "t", "./test", "Path for storing the test configuration")
|
||||||
|
v.BindPFlag("test.test-path", flags.Lookup("test-path"))
|
||||||
|
v.SetDefault("test.test-path", "./test")
|
||||||
|
|
||||||
|
return createCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *creator) config(v *viper.Viper, cmd *cobra.Command) error {
|
||||||
|
allOpt, err := option.ParseOptions(c.optionFile, v, cmd.PersistentFlags())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.TestOptions = allOpt.Test
|
||||||
|
|
||||||
|
c.w = cmd.OutOrStdout()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *creator) create() (err error) {
|
||||||
|
source, err := utils.GetAbsolutePath(c.FromPath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "invalid build products path %q", c.FromPath)
|
||||||
|
}
|
||||||
|
c.FromPath = source
|
||||||
|
|
||||||
|
target, err := utils.GetAbsolutePath(c.TestPath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "invalid test path %q", c.TestPath)
|
||||||
|
}
|
||||||
|
c.TestPath = target
|
||||||
|
|
||||||
|
fields := testTmplFields{}
|
||||||
|
|
||||||
|
// 1. extract the parameters from spec.yaml and convert them to PluginConf
|
||||||
|
path := fmt.Sprintf("%s/spec.yaml", c.FromPath)
|
||||||
|
spec, err := types.ParseSpecYAML(path)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to parse %s", path)
|
||||||
|
}
|
||||||
|
fields.PluginConf, err = config.ExtractPluginConfFrom(spec, "", "")
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to get the parameters of plugin-conf.yaml from %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. get DockerCompose instance
|
||||||
|
fields.DockerCompose = &DockerCompose{
|
||||||
|
TestPath: c.TestPath,
|
||||||
|
ProductPath: c.FromPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. get Envoy instance
|
||||||
|
var obj interface{}
|
||||||
|
conf := spec.GetConfigExample()
|
||||||
|
err = yaml.Unmarshal([]byte(conf), &obj)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to get the example of wasm plugin")
|
||||||
|
}
|
||||||
|
b, err := json.MarshalIndent(obj, "", strings.Repeat(" ", 2))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to marshal example to json")
|
||||||
|
}
|
||||||
|
jsExample := utils.AddIndent(string(b), strings.Repeat(" ", 30))
|
||||||
|
fields.Envoy = &Envoy{JSONExample: jsExample}
|
||||||
|
|
||||||
|
// 4. generate corresponding test files
|
||||||
|
if err = os.MkdirAll(target, 0755); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to create the test environment")
|
||||||
|
|
||||||
|
}
|
||||||
|
if err = c.genTestConfFiles(fields); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to create the test environment")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(c.w, "Created the test environment in %q\n", target)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type testTmplFields struct {
|
||||||
|
PluginConf *config.PluginConf // for plugin-conf.yaml
|
||||||
|
DockerCompose *DockerCompose // for docker-compose.yaml
|
||||||
|
Envoy *Envoy // for envoy.yaml
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *creator) genTestConfFiles(fields testTmplFields) (err error) {
|
||||||
|
if err = config.GenPluginConfYAML(fields.PluginConf, c.TestPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = genDockerComposeYAML(fields.DockerCompose, c.TestPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = genEnvoyYAML(fields.Envoy, c.TestPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user