mirror of
https://github.com/alibaba/higress.git
synced 2026-02-25 13:10:50 +08:00
Compare commits
36 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 |
20
.github/workflows/build-and-test.yaml
vendored
20
.github/workflows/build-and-test.yaml
vendored
@@ -38,10 +38,9 @@ jobs:
|
||||
path: |-
|
||||
envoy
|
||||
istio
|
||||
external
|
||||
.git/modules
|
||||
key: ${{ runner.os }}-submodules-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-submodules
|
||||
key: ${{ runner.os }}-submodules-new-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-submodules-new
|
||||
|
||||
- run: git stash # restore patch
|
||||
|
||||
@@ -85,10 +84,9 @@ jobs:
|
||||
path: |-
|
||||
envoy
|
||||
istio
|
||||
external
|
||||
.git/modules
|
||||
key: ${{ runner.os }}-submodules-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-submodules
|
||||
key: ${{ runner.os }}-submodules-new-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-submodules-new
|
||||
|
||||
- run: git stash # restore patch
|
||||
|
||||
@@ -134,10 +132,9 @@ jobs:
|
||||
path: |-
|
||||
envoy
|
||||
istio
|
||||
external
|
||||
.git/modules
|
||||
key: ${{ runner.os }}-submodules-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-submodules
|
||||
key: ${{ runner.os }}-submodules-new-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-submodules-new
|
||||
|
||||
- run: git stash # restore patch
|
||||
|
||||
@@ -175,10 +172,9 @@ jobs:
|
||||
path: |-
|
||||
envoy
|
||||
istio
|
||||
external
|
||||
.git/modules
|
||||
key: ${{ runner.os }}-submodules-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-submodules
|
||||
key: ${{ runner.os }}-submodules-new-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-submodules-new
|
||||
|
||||
- run: git stash # restore patch
|
||||
|
||||
|
||||
2
.github/workflows/build-image-and-push.yaml
vendored
2
.github/workflows/build-image-and-push.yaml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
envoy
|
||||
istio
|
||||
.git/modules
|
||||
key: ${{ runner.os }}-submodules-${{ github.run_id }}
|
||||
key: ${{ runner.os }}-submodules-new-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-submodules-new
|
||||
|
||||
- name: Calculate Docker metadata
|
||||
|
||||
@@ -27,6 +27,7 @@ header:
|
||||
- 'tools/'
|
||||
- 'test/README.md'
|
||||
- 'pkg/cmd/hgctl/testdata/config'
|
||||
- 'pkg/cmd/hgctl/manifests'
|
||||
|
||||
comment: on-failure
|
||||
dependency:
|
||||
|
||||
@@ -79,15 +79,15 @@ $(ARM64_OUT_LINUX)/higress:
|
||||
GOPROXY=$(GOPROXY) GOOS=linux GOARCH=arm64 LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh ./out/linux_arm64/ $(HIGRESS_BINARIES)
|
||||
|
||||
.PHONY: build-hgctl
|
||||
build-hgctl: $(OUT)
|
||||
build-hgctl: prebuild $(OUT)
|
||||
GOPROXY=$(GOPROXY) GOOS=$(GOOS_LOCAL) GOARCH=$(GOARCH_LOCAL) LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh $(OUT)/ $(HGCTL_BINARIES)
|
||||
|
||||
.PHONY: build-linux-hgctl
|
||||
build-linux-hgctl: $(OUT)
|
||||
build-linux-hgctl: prebuild $(OUT)
|
||||
GOPROXY=$(GOPROXY) GOOS=linux GOARCH=$(GOARCH_LOCAL) LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh $(OUT_LINUX)/ $(HGCTL_BINARIES)
|
||||
|
||||
.PHONY: build-hgctl-multiarch
|
||||
build-hgctl-multiarch: $(OUT)
|
||||
build-hgctl-multiarch: prebuild $(OUT)
|
||||
GOPROXY=$(GOPROXY) GOOS=linux GOARCH=amd64 LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh ./out/linux_amd64/ $(HGCTL_BINARIES)
|
||||
GOPROXY=$(GOPROXY) GOOS=linux GOARCH=arm64 LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh ./out/linux_arm64/ $(HGCTL_BINARIES)
|
||||
GOPROXY=$(GOPROXY) GOOS=darwin GOARCH=amd64 LDFLAGS=$(RELEASE_LDFLAGS) tools/hack/gobuild.sh ./out/darwin_amd64/ $(HGCTL_BINARIES)
|
||||
@@ -137,28 +137,30 @@ export ENVOY_TAR_PATH:=/home/package/envoy.tar.gz
|
||||
|
||||
external/package/envoy-amd64.tar.gz:
|
||||
# cd external/proxy; BUILD_WITH_CONTAINER=1 make test_release
|
||||
cd external/package; wget "https://github.com/alibaba/higress/releases/download/v1.0.0/envoy-amd64.tar.gz"
|
||||
cd external/package; wget "https://github.com/alibaba/higress/releases/download/v1.2.0/envoy-amd64.tar.gz"
|
||||
|
||||
external/package/envoy-arm64.tar.gz:
|
||||
# cd external/proxy; BUILD_WITH_CONTAINER=1 make test_release
|
||||
cd external/package; wget "https://github.com/alibaba/higress/releases/download/v1.0.0/envoy-arm64.tar.gz"
|
||||
|
||||
cd external/package; wget "https://github.com/alibaba/higress/releases/download/v1.2.0/envoy-arm64.tar.gz"
|
||||
|
||||
build-pilot:
|
||||
cd external/istio; rm -rf out/linux_amd64; GOOS_LOCAL=linux TARGET_OS=linux TARGET_ARCH=amd64 BUILD_WITH_CONTAINER=1 make build-linux
|
||||
cd external/istio; rm -rf out/linux_arm64; GOOS_LOCAL=linux TARGET_OS=linux TARGET_ARCH=arm64 BUILD_WITH_CONTAINER=1 make build-linux
|
||||
|
||||
build-pilot-local:
|
||||
cd external/istio; rm -rf out/linux_${GOARCH_LOCAL}; GOOS_LOCAL=linux TARGET_OS=linux TARGET_ARCH=${GOARCH_LOCAL} BUILD_WITH_CONTAINER=1 make build-linux
|
||||
|
||||
build-gateway: prebuild external/package/envoy-amd64.tar.gz external/package/envoy-arm64.tar.gz build-pilot
|
||||
cd external/istio; BUILD_WITH_CONTAINER=1 BUILDX_PLATFORM=true DOCKER_BUILD_VARIANTS=default DOCKER_TARGETS="docker.proxyv2" make docker
|
||||
|
||||
build-gateway-local: prebuild external/package/envoy-amd64.tar.gz external/package/envoy-arm64.tar.gz build-pilot
|
||||
cd external/istio; BUILD_WITH_CONTAINER=1 BUILDX_PLATFORM=false DOCKER_BUILD_VARIANTS=default DOCKER_TARGETS="docker.proxyv2" make docker
|
||||
cd external/istio; rm -rf out/linux_${GOARCH_LOCAL}; GOOS_LOCAL=linux TARGET_OS=linux BUILD_WITH_CONTAINER=1 BUILDX_PLATFORM=false DOCKER_BUILD_VARIANTS=default DOCKER_TARGETS="docker.proxyv2" make docker
|
||||
|
||||
build-istio: prebuild build-pilot
|
||||
cd external/istio; BUILD_WITH_CONTAINER=1 BUILDX_PLATFORM=true DOCKER_BUILD_VARIANTS=default DOCKER_TARGETS="docker.pilot" make docker
|
||||
|
||||
build-istio-local: prebuild build-pilot
|
||||
cd external/istio; BUILD_WITH_CONTAINER=1 BUILDX_PLATFORM=false DOCKER_BUILD_VARIANTS=default DOCKER_TARGETS="docker.pilot" make docker
|
||||
build-istio-local: prebuild build-pilot-local
|
||||
cd external/istio; rm -rf out/linux_${GOARCH_LOCAL}; GOOS_LOCAL=linux TARGET_OS=linux BUILD_WITH_CONTAINER=1 BUILDX_PLATFORM=false DOCKER_BUILD_VARIANTS=default DOCKER_TARGETS="docker.pilot" make docker
|
||||
|
||||
build-wasmplugins:
|
||||
./tools/hack/build-wasm-plugins.sh
|
||||
@@ -174,8 +176,8 @@ install: pre-install
|
||||
cd helm/higress; helm dependency build
|
||||
helm install higress helm/higress -n higress-system --create-namespace --set 'global.local=true'
|
||||
|
||||
ENVOY_LATEST_IMAGE_TAG ?= 1.2.0
|
||||
ISTIO_LATEST_IMAGE_TAG ?= 1.2.0
|
||||
ENVOY_LATEST_IMAGE_TAG ?= sha-6835486
|
||||
ISTIO_LATEST_IMAGE_TAG ?= sha-6835486
|
||||
|
||||
install-dev: pre-install
|
||||
helm install higress helm/core -n higress-system --create-namespace --set 'controller.tag=$(TAG)' --set 'gateway.replicas=1' --set 'pilot.tag=$(ISTIO_LATEST_IMAGE_TAG)' --set 'gateway.tag=$(ENVOY_LATEST_IMAGE_TAG)' --set 'global.local=true'
|
||||
@@ -255,16 +257,16 @@ delete-cluster: $(tools/kind) ## Delete kind cluster.
|
||||
.PHONY: kube-load-image
|
||||
kube-load-image: $(tools/kind) ## Install the Higress image to a kind cluster using the provided $IMAGE and $TAG.
|
||||
tools/hack/kind-load-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/higress $(TAG)
|
||||
tools/hack/docker-pull-image.sh registry.cn-hangzhou.aliyuncs.com/hinsteny/dubbo-provider-demo 0.0.1
|
||||
tools/hack/docker-pull-image.sh registry.cn-hangzhou.aliyuncs.com/hinsteny/nacos-standlone-rc3 1.0.0-RC3
|
||||
tools/hack/docker-pull-image.sh docker.io/alihigress/dubbo-provider-demo 0.0.1
|
||||
tools/hack/docker-pull-image.sh docker.io/alihigress/nacos-standlone-rc3 1.0.0-RC3
|
||||
tools/hack/docker-pull-image.sh docker.io/hashicorp/consul 1.16.0
|
||||
tools/hack/docker-pull-image.sh docker.io/charlie1380/eureka-registry-provider v0.3.0
|
||||
tools/hack/docker-pull-image.sh docker.io/bitinit/eureka latest
|
||||
tools/hack/docker-pull-image.sh registry.cn-hangzhou.aliyuncs.com/2456868764/httpbin 1.0.2
|
||||
tools/hack/kind-load-image.sh registry.cn-hangzhou.aliyuncs.com/hinsteny/dubbo-provider-demo 0.0.1
|
||||
tools/hack/kind-load-image.sh registry.cn-hangzhou.aliyuncs.com/hinsteny/nacos-standlone-rc3 1.0.0-RC3
|
||||
tools/hack/docker-pull-image.sh docker.io/alihigress/httpbin 1.0.2
|
||||
tools/hack/kind-load-image.sh docker.io/alihigress/dubbo-provider-demo 0.0.1
|
||||
tools/hack/kind-load-image.sh docker.io/alihigress/nacos-standlone-rc3 1.0.0-RC3
|
||||
tools/hack/kind-load-image.sh docker.io/hashicorp/consul 1.16.0
|
||||
tools/hack/kind-load-image.sh registry.cn-hangzhou.aliyuncs.com/2456868764/httpbin 1.0.2
|
||||
tools/hack/kind-load-image.sh docker.io/alihigress/httpbin 1.0.2
|
||||
tools/hack/kind-load-image.sh docker.io/charlie1380/eureka-registry-provider v0.3.0
|
||||
tools/hack/kind-load-image.sh docker.io/bitinit/eureka latest
|
||||
# run-higress-e2e-test starts to run ingress e2e tests.
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
182
go.mod
182
go.mod
@@ -10,17 +10,22 @@ replace github.com/chzyer/logex => github.com/chzyer/logex v1.1.11-0.20170329064
|
||||
// Avoid pulling in incompatible libraries
|
||||
replace github.com/docker/distribution => github.com/docker/distribution v0.0.0-20191216044856-a8371794149d
|
||||
|
||||
replace github.com/docker/docker => github.com/moby/moby v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible
|
||||
|
||||
// Client-go does not handle different versions of mergo due to some breaking changes - use the matching version
|
||||
replace github.com/imdario/mergo => github.com/imdario/mergo v0.3.5
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.6
|
||||
github.com/agiledragon/gomonkey/v2 v2.9.0
|
||||
github.com/avast/retry-go/v4 v4.3.4
|
||||
github.com/compose-spec/compose-go v1.8.2
|
||||
github.com/docker/cli v20.10.20+incompatible
|
||||
github.com/docker/compose/v2 v2.0.0-00010101000000-000000000000
|
||||
github.com/docker/docker v20.10.20+incompatible
|
||||
github.com/dubbogo/go-zookeeper v1.0.4-0.20211212162352-f9d2183d89d5
|
||||
github.com/dubbogo/gost v1.13.1
|
||||
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1
|
||||
github.com/fatih/color v1.14.1
|
||||
github.com/fatih/structtag v1.2.0
|
||||
github.com/gogo/protobuf v1.3.2
|
||||
github.com/golang/protobuf v1.5.2
|
||||
github.com/google/go-cmp v0.5.9
|
||||
@@ -28,25 +33,31 @@ require (
|
||||
github.com/hashicorp/consul/api v1.23.0
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hudl/fargo v1.4.0
|
||||
github.com/iancoleman/orderedmap v0.3.0
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/nacos-group/nacos-sdk-go v1.0.8
|
||||
github.com/nacos-group/nacos-sdk-go/v2 v2.1.2
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/spf13/cobra v1.2.1
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.8.1
|
||||
github.com/stretchr/testify v1.8.3
|
||||
go.uber.org/atomic v1.9.0
|
||||
google.golang.org/grpc v1.48.0
|
||||
google.golang.org/protobuf v1.28.0
|
||||
google.golang.org/protobuf v1.28.1
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
istio.io/api v0.0.0-20211122181927-8da52c66ff23
|
||||
istio.io/client-go v1.12.0-rc.1.0.20211118171212-b744b6f111e4
|
||||
istio.io/gogo-genproto v0.0.0-20211115195057-0e34bdd2be67
|
||||
istio.io/istio v0.0.0
|
||||
istio.io/pkg v0.0.0-20211115195056-e379f31ee62a
|
||||
k8s.io/api v0.22.5
|
||||
k8s.io/apimachinery v0.22.5
|
||||
k8s.io/api v0.24.1
|
||||
k8s.io/apimachinery v0.24.1
|
||||
k8s.io/cli-runtime v0.22.2
|
||||
k8s.io/client-go v0.22.5
|
||||
k8s.io/client-go v0.24.1
|
||||
k8s.io/kubectl v0.22.2
|
||||
sigs.k8s.io/controller-runtime v0.10.2
|
||||
sigs.k8s.io/yaml v1.3.0
|
||||
@@ -69,56 +80,63 @@ require (
|
||||
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.2 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.0 // indirect
|
||||
github.com/Microsoft/hcsshim v0.8.21 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||
github.com/Microsoft/hcsshim v0.9.6 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/RageCage64/multilinediff v0.2.0 // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1704 // indirect
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
|
||||
github.com/aws/aws-sdk-go v1.41.7 // indirect
|
||||
github.com/aws/aws-sdk-go v1.43.16 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect
|
||||
github.com/braydonk/yaml v0.7.0 // indirect
|
||||
github.com/buger/goterm v1.0.4 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
|
||||
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect
|
||||
github.com/clbanning/mxj v1.8.4 // indirect
|
||||
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20220520190051-1e77728a1eaa // indirect
|
||||
github.com/containerd/containerd v1.5.7 // indirect
|
||||
github.com/containerd/continuity v0.1.0 // indirect
|
||||
github.com/compose-spec/godotenv v1.1.1 // indirect
|
||||
github.com/containerd/cgroups v1.0.4 // indirect
|
||||
github.com/containerd/console v1.0.3 // indirect
|
||||
github.com/containerd/containerd v1.6.14 // indirect
|
||||
github.com/containerd/continuity v0.3.0 // indirect
|
||||
github.com/containerd/typeurl v1.0.2 // indirect
|
||||
github.com/coreos/go-oidc/v3 v3.1.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0 // indirect
|
||||
github.com/docker/cli v20.10.7+incompatible // indirect
|
||||
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||
github.com/docker/docker v20.10.7+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.6.3 // indirect
|
||||
github.com/distribution/distribution/v3 v3.0.0-20221201083218-92d136e113cf // indirect
|
||||
github.com/docker/buildx v0.9.1 // indirect
|
||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.7.0 // indirect
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0 // indirect
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
|
||||
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
|
||||
github.com/fatih/color v1.14.1 // indirect
|
||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/fvbommel/sortorder v1.0.1 // indirect
|
||||
github.com/fvbommel/sortorder v1.0.2 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-errors/errors v1.0.1 // indirect
|
||||
github.com/go-kit/log v0.1.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.5.0 // indirect
|
||||
github.com/go-logr/logr v1.2.2 // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.5 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/goccy/go-json v0.4.8 // indirect
|
||||
github.com/gofrs/flock v0.8.0 // indirect
|
||||
github.com/gogo/googleapis v1.4.1 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
@@ -133,23 +151,29 @@ require (
|
||||
github.com/gosuri/uitable v0.0.4 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-hclog v1.5.0 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||
github.com/hashicorp/go-version v1.3.0 // indirect
|
||||
github.com/hashicorp/go-version v1.6.0 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hashicorp/serf v0.10.1 // indirect
|
||||
github.com/huandu/xstrings v1.3.2 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/imdario/mergo v0.3.13 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/jaguilar/vt100 v0.0.0-20150826170717-2703a27b14ea // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/jmoiron/sqlx v1.3.1 // indirect
|
||||
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.13.6 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/klauspost/compress v1.15.9 // indirect
|
||||
github.com/kr/pretty v0.3.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.7 // indirect
|
||||
@@ -162,20 +186,27 @@ require (
|
||||
github.com/lestrrat/go-strftime v0.0.0-20180220042222-ba3bf9c1d042 // indirect
|
||||
github.com/lib/pq v1.10.0 // indirect
|
||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
||||
github.com/magiconair/properties v1.8.5 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.12 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/mattn/go-shellwords v1.0.12 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
||||
github.com/miekg/dns v1.1.43 // indirect
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/buildkit v0.10.4 // indirect
|
||||
github.com/moby/locker v1.0.1 // indirect
|
||||
github.com/moby/spdystream v0.2.0 // indirect
|
||||
github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 // indirect
|
||||
github.com/moby/sys/mount v0.3.0 // indirect
|
||||
github.com/moby/sys/mountinfo v0.6.0 // indirect
|
||||
github.com/moby/sys/signal v0.7.0 // indirect
|
||||
github.com/moby/sys/symlink v0.2.0 // indirect
|
||||
github.com/moby/term v0.0.0-20221128092401-c43b287e0e0f // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
|
||||
@@ -183,9 +214,11 @@ require (
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible // indirect
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.1 // indirect
|
||||
github.com/opencontainers/runc v1.0.2 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
|
||||
github.com/opencontainers/runc v1.1.3 // indirect
|
||||
github.com/openshift/api v0.0.0-20200713203337-b2494ecb17dd // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pelletier/go-toml v1.9.4 // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.12.2 // indirect
|
||||
@@ -194,12 +227,19 @@ require (
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/prometheus/statsd_exporter v0.21.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.6.1 // indirect
|
||||
github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc // indirect
|
||||
github.com/russross/blackfriday v1.5.2 // indirect
|
||||
github.com/russross/blackfriday v1.6.0 // indirect
|
||||
github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/spf13/afero v1.2.2 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/theupdateframework/notary v0.7.0 // indirect
|
||||
github.com/tonistiigi/fsutil v0.0.0-20220930225714-4638ad635be5 // indirect
|
||||
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
|
||||
github.com/toolkits/concurrent v0.0.0-20150624120057-a4371d70e3e3 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
@@ -207,14 +247,14 @@ require (
|
||||
github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect
|
||||
github.com/yl2chen/cidranger v1.0.2 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.7.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.12.0 // indirect
|
||||
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
|
||||
go.uber.org/multierr v1.7.0 // indirect
|
||||
go.uber.org/zap v1.21.0 // indirect
|
||||
golang.org/x/crypto v0.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
|
||||
golang.org/x/net v0.12.0 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
|
||||
golang.org/x/oauth2 v0.6.0 // indirect
|
||||
golang.org/x/sync v0.2.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/term v0.10.0 // indirect
|
||||
@@ -225,7 +265,7 @@ require (
|
||||
gomodules.xyz/orderedmap v0.1.0 // indirect
|
||||
google.golang.org/api v0.61.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 // indirect
|
||||
gopkg.in/gcfg.v1 v1.2.3 // indirect
|
||||
gopkg.in/gorp.v1 v1.7.2 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
@@ -233,10 +273,9 @@ require (
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/apiserver v0.22.2 // indirect
|
||||
k8s.io/component-base v0.22.2 // indirect
|
||||
k8s.io/klog/v2 v2.40.1 // indirect
|
||||
k8s.io/apiserver v0.22.5 // indirect
|
||||
k8s.io/component-base v0.22.5 // indirect
|
||||
k8s.io/klog/v2 v2.60.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c // indirect
|
||||
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect
|
||||
oras.land/oras-go v0.4.0 // indirect
|
||||
@@ -259,23 +298,6 @@ replace istio.io/client-go => ./external/client-go
|
||||
|
||||
replace istio.io/istio => ./external/istio
|
||||
|
||||
replace (
|
||||
github.com/go-logr/logr => github.com/go-logr/logr v0.4.0
|
||||
k8s.io/api => k8s.io/api v0.22.2
|
||||
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.22.2
|
||||
k8s.io/apimachinery => k8s.io/apimachinery v0.22.2
|
||||
k8s.io/cli-runtime => k8s.io/cli-runtime v0.22.2
|
||||
k8s.io/client-go => k8s.io/client-go v0.22.2
|
||||
k8s.io/component-base => k8s.io/component-base v0.22.2
|
||||
k8s.io/klog/v2 => k8s.io/klog/v2 v2.10.0
|
||||
k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20211020163157-7327e2aaee2b // indirect
|
||||
k8s.io/kubectl => k8s.io/kubectl v0.22.2
|
||||
k8s.io/utils => k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect
|
||||
sigs.k8s.io/gateway-api => sigs.k8s.io/gateway-api v0.4.0 // indirect
|
||||
sigs.k8s.io/kustomize/api => sigs.k8s.io/kustomize/api v0.8.11 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml => sigs.k8s.io/kustomize/kyaml v0.11.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/evanphx/json-patch/v5 v5.6.0
|
||||
github.com/google/yamlfmt v0.10.0
|
||||
@@ -285,3 +307,49 @@ require (
|
||||
knative.dev/networking v0.0.0-20220302134042-e8b2eb995165
|
||||
knative.dev/pkg v0.0.0-20220301181942-2fdd5f232e77
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/Sirupsen/logrus => github.com/sirupsen/logrus v1.9.3
|
||||
github.com/go-logr/logr => github.com/go-logr/logr v0.4.0
|
||||
|
||||
k8s.io/api => k8s.io/api v0.22.2
|
||||
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.22.2
|
||||
k8s.io/apimachinery => k8s.io/apimachinery v0.22.2
|
||||
k8s.io/cli-runtime => k8s.io/cli-runtime v0.22.2
|
||||
k8s.io/client-go => k8s.io/client-go v0.22.2
|
||||
k8s.io/code-generator => k8s.io/code-generator v0.22.2
|
||||
k8s.io/component-base => k8s.io/component-base v0.22.2
|
||||
k8s.io/component-helpers => k8s.io/component-helpers v0.22.2
|
||||
k8s.io/klog/v2 => k8s.io/klog/v2 v2.10.0
|
||||
k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e
|
||||
k8s.io/kubectl => k8s.io/kubectl v0.22.2
|
||||
k8s.io/metrics => k8s.io/metrics v0.22.2
|
||||
|
||||
k8s.io/utils => k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect
|
||||
sigs.k8s.io/kustomize/api => sigs.k8s.io/kustomize/api v0.8.11 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml => sigs.k8s.io/kustomize/kyaml v0.11.0 // indirect
|
||||
)
|
||||
|
||||
// for pkg/cmd/hgctl/docker/compose.go
|
||||
// TODO(WeixinX): Wait for the dependency library to upgrade, such as github.com/go-logr/logr from v0.4.0 to v1.2+
|
||||
// replace (
|
||||
// github.com/compose-spec/compose-go => github.com/compose-spec/compose-go v1.8.2
|
||||
// github.com/cucumber/godog => github.com/laurazard/godog v0.0.0-20220922095256-4c4b17abdae7
|
||||
// github.com/docker/buildx => github.com/docker/buildx v0.9.1
|
||||
// github.com/docker/cli => github.com/docker/cli v20.10.3-0.20221013132413-1d6c6e2367e2+incompatible
|
||||
// github.com/docker/compose/v2 => github.com/docker/compose/v2 v2.15.1
|
||||
// github.com/docker/docker => github.com/moby/moby v20.10.3-0.20221021173910-5aac513617f0+incompatible
|
||||
// github.com/moby/buildkit => github.com/moby/buildkit v0.10.1-0.20220816171719-55ba9d14360a
|
||||
// )
|
||||
|
||||
replace (
|
||||
github.com/compose-spec/compose-go => github.com/compose-spec/compose-go v1.0.8
|
||||
github.com/docker/buildx => github.com/docker/buildx v0.5.2-0.20210422185057-908a856079fc
|
||||
github.com/docker/cli => github.com/docker/cli v20.10.7+incompatible
|
||||
github.com/docker/compose/v2 => github.com/docker/compose/v2 v2.2.0
|
||||
github.com/docker/docker => github.com/docker/docker v20.10.3+incompatible
|
||||
github.com/jaguilar/vt100 => github.com/tonistiigi/vt100 v0.0.0-20190402012908-ad4c4a574305
|
||||
github.com/moby/buildkit => github.com/moby/buildkit v0.8.2-0.20210401015549-df49b648c8bf
|
||||
github.com/tonistiigi/fsutil => github.com/tonistiigi/fsutil v0.0.0-20201103201449-0834f99b7b85
|
||||
sigs.k8s.io/gateway-api => github.com/johnlanni/gateway-api v0.0.0-20231031082632-72137664e7c7
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 1.2.0
|
||||
appVersion: 1.3.0
|
||||
description: Helm chart for deploying higress gateways
|
||||
icon: https://higress.io/img/higress_logo_small.png
|
||||
home: http://higress.io/
|
||||
@@ -10,4 +10,4 @@ name: higress-core
|
||||
sources:
|
||||
- http://github.com/alibaba/higress
|
||||
type: application
|
||||
version: 1.2.0
|
||||
version: 1.3.0
|
||||
|
||||
@@ -2,6 +2,7 @@ apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "controller.name" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "controller.labels" . | nindent 4 }}
|
||||
spec:
|
||||
|
||||
@@ -2,6 +2,7 @@ apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "controller.name" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "controller.labels" . | nindent 4 }}
|
||||
spec:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
dependencies:
|
||||
- name: higress-core
|
||||
repository: file://../core
|
||||
version: 1.2.0
|
||||
version: 1.3.0
|
||||
- name: higress-console
|
||||
repository: https://higress.io/helm-charts/
|
||||
version: 1.2.0
|
||||
digest: sha256:d53c2da70cb3bcace50bce756acb50750a84c0888ec0d4c112939bf3c6e4daeb
|
||||
generated: "2023-09-22T16:52:34.940675+08:00"
|
||||
version: 1.3.0
|
||||
digest: sha256:3efc59ad8cd92ab4c3c87abeed8e2fc0288bb3ecc2805888ba6eaaf265ba6a10
|
||||
generated: "2023-11-02T11:45:56.011629+08:00"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 1.2.0
|
||||
appVersion: 1.3.0
|
||||
description: Helm chart for deploying Higress gateways
|
||||
icon: https://higress.io/img/higress_logo_small.png
|
||||
home: http://higress.io/
|
||||
@@ -12,9 +12,9 @@ sources:
|
||||
dependencies:
|
||||
- name: higress-core
|
||||
repository: "file://../core"
|
||||
version: 1.2.0
|
||||
version: 1.3.0
|
||||
- name: higress-console
|
||||
repository: "https://higress.io/helm-charts/"
|
||||
version: 1.2.0
|
||||
version: 1.3.0
|
||||
type: application
|
||||
version: 1.2.0
|
||||
version: 1.3.0
|
||||
|
||||
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)
|
||||
@@ -30,10 +30,11 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
listenPort = 0
|
||||
promPort = 0
|
||||
grafanaPort = 0
|
||||
consolePort = 0
|
||||
listenPort = 0
|
||||
promPort = 0
|
||||
grafanaPort = 0
|
||||
consolePort = 0
|
||||
controllerPort = 0
|
||||
|
||||
bindAddress = "localhost"
|
||||
|
||||
@@ -54,6 +55,7 @@ const (
|
||||
defaultPrometheusPort = 9090
|
||||
defaultGrafanaPort = 3000
|
||||
defaultConsolePort = 8080
|
||||
defaultControllerPort = 8888
|
||||
)
|
||||
|
||||
func newDashboardCmd() *cobra.Command {
|
||||
@@ -99,6 +101,11 @@ func newDashboardCmd() *cobra.Command {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -265,6 +272,41 @@ func envoyDashCmd() *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// port-forward to Higress System Console; open browser
|
||||
func controllerDebugCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "controller",
|
||||
Short: "Open Controller debug web UI",
|
||||
Long: `Open Higress Controller`,
|
||||
Example: ` hgctl dashboard controller
|
||||
|
||||
# with short syntax
|
||||
hgctl dash controller
|
||||
hgctl d controller`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := kubernetes.NewCLIClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
|
||||
if err != nil {
|
||||
return fmt.Errorf("build CLI client fail: %w", err)
|
||||
}
|
||||
|
||||
pl, err := client.PodsForSelector(addonNamespace, "app=higress-controller")
|
||||
if err != nil {
|
||||
return fmt.Errorf("not able to locate controller pod: %v", err)
|
||||
}
|
||||
|
||||
if len(pl.Items) < 1 {
|
||||
return errors.New("no higress controller pods found")
|
||||
}
|
||||
|
||||
// only use the first pod in the list
|
||||
return portForward(pl.Items[0].Name, addonNamespace, "Controller",
|
||||
"http://%s/debug", bindAddress, controllerPort, client, cmd.OutOrStdout(), browser)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// portForward first tries to forward localhost:remotePort to podName:remotePort, falls back to dynamic local port
|
||||
func portForward(podName, namespace, flavor, urlFormat, localAddress string, remotePort int,
|
||||
client kubernetes.CLIClient, writer io.Writer, browser bool,
|
||||
|
||||
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{})
|
||||
}
|
||||
@@ -26,15 +26,40 @@ import (
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// ReadYamlProfile gets the overlay yaml file from list of files and return profile value from file overlay and set overlay.
|
||||
func ReadYamlProfile(inFilenames []string, setFlags []string) (string, string, error) {
|
||||
// GetProfileFromFlags get profile name from flags.
|
||||
func GetProfileFromFlags(setFlags []string) (string, error) {
|
||||
profileName := DefaultProfileName
|
||||
// The profile coming from --set flag has the highest precedence.
|
||||
psf := GetValueForSetFlag(setFlags, "profile")
|
||||
if psf != "" {
|
||||
profileName = psf
|
||||
}
|
||||
return "", profileName, nil
|
||||
return profileName, nil
|
||||
}
|
||||
|
||||
func GetValuesOverylayFromFiles(inFilenames []string) (string, error) {
|
||||
// Convert layeredYamls under values node in profile file to support helm values
|
||||
overLayYamls := ""
|
||||
// Get Overlays from files
|
||||
if len(inFilenames) > 0 {
|
||||
layeredYamls, err := ReadLayeredYAMLs(inFilenames)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
vals := make(map[string]any)
|
||||
if err := yaml.Unmarshal([]byte(layeredYamls), &vals); err != nil {
|
||||
return "", fmt.Errorf("%s:\n\nYAML:\n%s", err, layeredYamls)
|
||||
}
|
||||
values := make(map[string]any)
|
||||
values["values"] = vals
|
||||
out, err := yaml.Marshal(values)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
overLayYamls = string(out)
|
||||
}
|
||||
|
||||
return overLayYamls, nil
|
||||
}
|
||||
|
||||
func GetUninstallProfileName() string {
|
||||
@@ -101,12 +126,17 @@ func GenerateConfig(inFilenames []string, setFlags []string) (string, *Profile,
|
||||
return "", nil, "", err
|
||||
}
|
||||
|
||||
fy, profileName, err := ReadYamlProfile(inFilenames, setFlags)
|
||||
profileName, err := GetProfileFromFlags(setFlags)
|
||||
if err != nil {
|
||||
return "", nil, "", err
|
||||
}
|
||||
|
||||
profileString, profile, err := GenProfile(profileName, fy, setFlags)
|
||||
valuesOverlay, err := GetValuesOverylayFromFiles(inFilenames)
|
||||
if err != nil {
|
||||
return "", nil, "", err
|
||||
}
|
||||
|
||||
profileString, profile, err := GenProfile(profileName, valuesOverlay, setFlags)
|
||||
|
||||
if err != nil {
|
||||
return "", nil, "", err
|
||||
|
||||
@@ -17,7 +17,9 @@ package helm
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"istio.io/istio/operator/pkg/util"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
@@ -43,83 +45,70 @@ type Profile struct {
|
||||
}
|
||||
|
||||
type ProfileGlobal struct {
|
||||
Install InstallMode `json:"install,omitempty"`
|
||||
IngressClass string `json:"ingressClass,omitempty"`
|
||||
WatchNamespace string `json:"watchNamespace,omitempty"`
|
||||
DisableAlpnH2 bool `json:"disableAlpnH2,omitempty"`
|
||||
EnableStatus bool `json:"enableStatus,omitempty"`
|
||||
EnableIstioAPI bool `json:"enableIstioAPI,omitempty"`
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
IstioNamespace string `json:"istioNamespace,omitempty"`
|
||||
Install InstallMode `json:"install,omitempty"`
|
||||
IngressClass string `json:"ingressClass,omitempty"`
|
||||
EnableIstioAPI bool `json:"enableIstioAPI,omitempty"`
|
||||
EnableGatewayAPI bool `json:"enableGatewayAPI,omitempty"`
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
}
|
||||
|
||||
func (p ProfileGlobal) SetFlags(install InstallMode) ([]string, error) {
|
||||
sets := make([]string, 0)
|
||||
sets = append(sets, fmt.Sprintf("global.ingressClass=%s", p.IngressClass))
|
||||
sets = append(sets, fmt.Sprintf("global.watchNamespace=%s", p.WatchNamespace))
|
||||
sets = append(sets, fmt.Sprintf("global.disableAlpnH2=%t", p.DisableAlpnH2))
|
||||
sets = append(sets, fmt.Sprintf("global.enableStatus=%t", p.EnableStatus))
|
||||
sets = append(sets, fmt.Sprintf("global.enableIstioAPI=%t", p.EnableIstioAPI))
|
||||
sets = append(sets, fmt.Sprintf("global.istioNamespace=%s", p.IstioNamespace))
|
||||
if install == InstallLocalK8s {
|
||||
sets = append(sets, fmt.Sprintf("global.local=%t", true))
|
||||
if install == InstallK8s || install == InstallLocalK8s {
|
||||
sets = append(sets, fmt.Sprintf("global.ingressClass=%s", p.IngressClass))
|
||||
sets = append(sets, fmt.Sprintf("global.enableIstioAPI=%t", p.EnableIstioAPI))
|
||||
sets = append(sets, fmt.Sprintf("global.enableGatewayAPI=%t", p.EnableGatewayAPI))
|
||||
if install == InstallLocalK8s {
|
||||
sets = append(sets, fmt.Sprintf("global.local=%t", true))
|
||||
}
|
||||
}
|
||||
return sets, nil
|
||||
}
|
||||
|
||||
func (p ProfileGlobal) Validate(install InstallMode) []error {
|
||||
errs := make([]error, 0)
|
||||
// now only support k8s and local-k8s installation mode
|
||||
if p.Install != InstallK8s && p.Install != InstallLocalK8s {
|
||||
errs = append(errs, errors.New("global.install only can be set to k8s or local-k8s"))
|
||||
// now only support k8s, local-k8s, local-docker installation mode
|
||||
if install != InstallK8s && install != InstallLocalK8s && install != InstallLocalDocker {
|
||||
errs = append(errs, errors.New("global.install only can be set to k8s, local-k8s or local-docker"))
|
||||
}
|
||||
if len(p.IngressClass) == 0 {
|
||||
errs = append(errs, errors.New("global.ingressClass can't be empty"))
|
||||
}
|
||||
if len(p.Namespace) == 0 {
|
||||
errs = append(errs, errors.New("global.namespace can't be empty"))
|
||||
}
|
||||
if len(p.IstioNamespace) == 0 {
|
||||
errs = append(errs, errors.New("global.istioNamespace can't be empty"))
|
||||
if install == InstallK8s || install == InstallLocalK8s {
|
||||
if len(p.IngressClass) == 0 {
|
||||
errs = append(errs, errors.New("global.ingressClass can't be empty"))
|
||||
}
|
||||
if len(p.Namespace) == 0 {
|
||||
errs = append(errs, errors.New("global.namespace can't be empty"))
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
type ProfileConsole struct {
|
||||
Port uint32 `json:"port,omitempty"`
|
||||
Replicas uint32 `json:"replicas,omitempty"`
|
||||
ServiceType string `json:"serviceType,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
TlsSecretName string `json:"tlsSecretName,omitempty"`
|
||||
WebLoginPrompt string `json:"webLoginPrompt,omitempty"`
|
||||
AdminPasswordValue string `json:"adminPasswordValue,omitempty"`
|
||||
AdminPasswordLength uint32 `json:"adminPasswordLength,omitempty"`
|
||||
O11yEnabled bool `json:"o11YEnabled,omitempty"`
|
||||
PvcRwxSupported bool `json:"pvcRwxSupported,omitempty"`
|
||||
Port uint32 `json:"port,omitempty"`
|
||||
Replicas uint32 `json:"replicas,omitempty"`
|
||||
O11yEnabled bool `json:"o11YEnabled,omitempty"`
|
||||
}
|
||||
|
||||
func (p ProfileConsole) SetFlags(install InstallMode) ([]string, error) {
|
||||
sets := make([]string, 0)
|
||||
sets = append(sets, fmt.Sprintf("higress-console.replicaCount=%d", p.Replicas))
|
||||
sets = append(sets, fmt.Sprintf("higress-console.service.type=%s", p.ServiceType))
|
||||
sets = append(sets, fmt.Sprintf("higress-console.domain=%s", p.Domain))
|
||||
sets = append(sets, fmt.Sprintf("higress-console.tlsSecretName=%s", p.TlsSecretName))
|
||||
sets = append(sets, fmt.Sprintf("higress-console.web.login.prompt=%s", p.WebLoginPrompt))
|
||||
sets = append(sets, fmt.Sprintf("higress-console.admin.password.value=%s", p.AdminPasswordValue))
|
||||
sets = append(sets, fmt.Sprintf("higress-console.admin.password.length=%d", p.AdminPasswordLength))
|
||||
sets = append(sets, fmt.Sprintf("higress-console.o11y.enabled=%t", p.O11yEnabled))
|
||||
sets = append(sets, fmt.Sprintf("higress-console.pvc.rwxSupported=%t", p.PvcRwxSupported))
|
||||
if install == InstallK8s || install == InstallLocalK8s {
|
||||
sets = append(sets, fmt.Sprintf("higress-console.replicaCount=%d", p.Replicas))
|
||||
sets = append(sets, fmt.Sprintf("higress-console.o11y.enabled=%t", p.O11yEnabled))
|
||||
}
|
||||
return sets, nil
|
||||
}
|
||||
|
||||
func (p ProfileConsole) Validate(install InstallMode) []error {
|
||||
errs := make([]error, 0)
|
||||
if p.Replicas <= 0 {
|
||||
errs = append(errs, errors.New("console.replica need be large than zero"))
|
||||
if install == InstallK8s || install == InstallLocalK8s {
|
||||
if p.Replicas <= 0 {
|
||||
errs = append(errs, errors.New("console.replica need be large than zero"))
|
||||
}
|
||||
}
|
||||
|
||||
if p.ServiceType != "ClusterIP" && p.ServiceType != "NodePort" && p.ServiceType != "LoadBalancer" {
|
||||
errs = append(errs, errors.New("console.serviceType can only be set to ClusterIP, NodePort or LoadBalancer"))
|
||||
if install == InstallLocalDocker {
|
||||
if p.Port <= 0 {
|
||||
errs = append(errs, errors.New("console.port need be large than zero"))
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
@@ -134,16 +123,31 @@ type ProfileGateway struct {
|
||||
|
||||
func (p ProfileGateway) SetFlags(install InstallMode) ([]string, error) {
|
||||
sets := make([]string, 0)
|
||||
sets = append(sets, fmt.Sprintf("higress-core.gateway.replicas=%d", p.Replicas))
|
||||
if install == InstallK8s || install == InstallLocalK8s {
|
||||
sets = append(sets, fmt.Sprintf("higress-core.gateway.replicas=%d", p.Replicas))
|
||||
}
|
||||
return sets, nil
|
||||
}
|
||||
|
||||
func (p ProfileGateway) Validate(install InstallMode) []error {
|
||||
errs := make([]error, 0)
|
||||
if p.Replicas <= 0 {
|
||||
errs = append(errs, errors.New("gateway.replica need be large than zero"))
|
||||
if install == InstallK8s || install == InstallLocalK8s {
|
||||
if p.Replicas <= 0 {
|
||||
errs = append(errs, errors.New("gateway.replica need be large than zero"))
|
||||
}
|
||||
}
|
||||
|
||||
if install == InstallLocalDocker {
|
||||
if p.HttpPort <= 0 {
|
||||
errs = append(errs, errors.New("gateway.httpPort need be large than zero"))
|
||||
}
|
||||
if p.HttpsPort <= 0 {
|
||||
errs = append(errs, errors.New("gateway.httpsPort need be large than zero"))
|
||||
}
|
||||
if p.MetricsPort <= 0 {
|
||||
errs = append(errs, errors.New("gateway.MetricsPort need be large than zero"))
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
@@ -153,16 +157,19 @@ type ProfileController struct {
|
||||
|
||||
func (p ProfileController) SetFlags(install InstallMode) ([]string, error) {
|
||||
sets := make([]string, 0)
|
||||
sets = append(sets, fmt.Sprintf("higress-core.controller.replicas=%d", p.Replicas))
|
||||
if install == InstallK8s || install == InstallLocalK8s {
|
||||
sets = append(sets, fmt.Sprintf("higress-core.controller.replicas=%d", p.Replicas))
|
||||
}
|
||||
return sets, nil
|
||||
}
|
||||
|
||||
func (p ProfileController) Validate(install InstallMode) []error {
|
||||
errs := make([]error, 0)
|
||||
if p.Replicas <= 0 {
|
||||
errs = append(errs, errors.New("controller.replica need be large than zero"))
|
||||
if install == InstallK8s || install == InstallLocalK8s {
|
||||
if p.Replicas <= 0 {
|
||||
errs = append(errs, errors.New("controller.replica need be large than zero"))
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
@@ -176,6 +183,31 @@ type ProfileStorage struct {
|
||||
|
||||
func (p ProfileStorage) Validate(install InstallMode) []error {
|
||||
errs := make([]error, 0)
|
||||
if install == InstallLocalDocker {
|
||||
if len(p.Url) == 0 {
|
||||
errs = append(errs, errors.New("storage.url can't be empty"))
|
||||
}
|
||||
if len(p.Ns) == 0 {
|
||||
errs = append(errs, errors.New("storage.ns can't be empty"))
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(p.Url, "nacos://") && !strings.HasPrefix(p.Url, "file://") {
|
||||
errs = append(errs, fmt.Errorf("invalid storage url: %s", p.Url))
|
||||
} else {
|
||||
// check localhost or 127.0.0.0
|
||||
if strings.Contains(p.Url, "localhost") || strings.Contains(p.Url, "/127.") {
|
||||
errs = append(errs, errors.New("localhost or loopback addresses in nacos url won't work"))
|
||||
}
|
||||
}
|
||||
|
||||
if len(p.DataEncKey) > 0 && len(p.DataEncKey) != 32 {
|
||||
errs = append(errs, fmt.Errorf("expecting 32 characters for dataEncKey, but got %d length", len(p.DataEncKey)))
|
||||
}
|
||||
|
||||
if len(p.Username) > 0 && len(p.Password) == 0 || len(p.Username) == 0 && len(p.Password) > 0 {
|
||||
errs = append(errs, errors.New("both nacos username and password should be provided"))
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
@@ -186,8 +218,8 @@ type Chart struct {
|
||||
}
|
||||
|
||||
type ProfileCharts struct {
|
||||
Higress Chart `json:"higress,omitempty"`
|
||||
Istio Chart `json:"istio,omitempty"`
|
||||
Higress Chart `json:"higress,omitempty"`
|
||||
Standalone Chart `json:"standalone,omitempty"`
|
||||
}
|
||||
|
||||
func (p ProfileCharts) Validate(install InstallMode) []error {
|
||||
@@ -222,8 +254,13 @@ func (p *Profile) ValuesYaml() (string, error) {
|
||||
}
|
||||
valueOverlayYAML = string(out)
|
||||
}
|
||||
|
||||
flagsYAML, err := overlaySetFlagValues("", setFlags)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// merge values and setFlags
|
||||
overlayYAML, err := overlaySetFlagValues(valueOverlayYAML, setFlags)
|
||||
overlayYAML, err := util.OverlayYAML(flagsYAML, valueOverlayYAML)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -237,6 +274,26 @@ func (p *Profile) IstioEnabled() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Profile) GatewayAPIEnabled() bool {
|
||||
if (p.Global.Install == InstallK8s || p.Global.Install == InstallLocalK8s) && p.Global.EnableGatewayAPI {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Profile) GetIstioNamespace() string {
|
||||
if valuesGlobal, ok1 := p.Values["global"]; ok1 {
|
||||
if global, ok2 := valuesGlobal.(map[string]any); ok2 {
|
||||
if istioNamespace, ok3 := global["istioNamespace"]; ok3 {
|
||||
if namespace, ok4 := istioNamespace.(string); ok4 {
|
||||
return namespace
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *Profile) Validate() error {
|
||||
errs := make([]error, 0)
|
||||
errsGlobal := p.Global.Validate(p.Global.Install)
|
||||
@@ -256,7 +313,7 @@ func (p *Profile) Validate() error {
|
||||
errs = append(errs, errsController...)
|
||||
}
|
||||
errsStorage := p.Storage.Validate(p.Global.Install)
|
||||
if len(errsController) > 0 {
|
||||
if len(errsStorage) > 0 {
|
||||
errs = append(errs, errsStorage...)
|
||||
}
|
||||
errsCharts := p.Charts.Validate(p.Global.Install)
|
||||
|
||||
@@ -127,7 +127,7 @@ type RendererOptions struct {
|
||||
Name string
|
||||
Namespace string
|
||||
|
||||
// fields for LocalRenderer
|
||||
// fields for LocalChartRenderer and LocalFileRenderer
|
||||
FS fs.FS
|
||||
Dir string
|
||||
|
||||
@@ -174,14 +174,84 @@ func WithRepoURL(repo string) RendererOption {
|
||||
}
|
||||
}
|
||||
|
||||
// LocalRenderer load chart from local file system
|
||||
type LocalRenderer struct {
|
||||
// LocalFileRenderer load yaml files from local file system
|
||||
type LocalFileRenderer struct {
|
||||
Opts *RendererOptions
|
||||
filesMap map[string]string
|
||||
Started bool
|
||||
}
|
||||
|
||||
func NewLocalFileRenderer(opts ...RendererOption) (Renderer, error) {
|
||||
newOpts := &RendererOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(newOpts)
|
||||
}
|
||||
|
||||
return &LocalFileRenderer{
|
||||
Opts: newOpts,
|
||||
filesMap: make(map[string]string),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *LocalFileRenderer) Init() error {
|
||||
fileNames, err := getFileNames(l.Opts.FS, l.Opts.Dir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("chart of component %s doesn't exist", l.Opts.Name)
|
||||
}
|
||||
return fmt.Errorf("getFileNames err: %s", err)
|
||||
}
|
||||
for _, fileName := range fileNames {
|
||||
data, err := fs.ReadFile(l.Opts.FS, fileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ReadFile %s err: %s", fileName, err)
|
||||
}
|
||||
|
||||
l.filesMap[fileName] = string(data)
|
||||
}
|
||||
l.Started = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LocalFileRenderer) RenderManifest(valsYaml string) (string, error) {
|
||||
if !l.Started {
|
||||
return "", errors.New("LocalFileRenderer has not been init")
|
||||
}
|
||||
keys := make([]string, 0, len(l.filesMap))
|
||||
for key := range l.filesMap {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
// to ensure that every manifest rendered by same values are the same
|
||||
sort.Strings(keys)
|
||||
|
||||
var builder strings.Builder
|
||||
for i := 0; i < len(keys); i++ {
|
||||
file := l.filesMap[keys[i]]
|
||||
file = util.ApplyFilters(file, DefaultFilters...)
|
||||
// ignore empty manifest
|
||||
if file == "" {
|
||||
continue
|
||||
}
|
||||
if !strings.HasSuffix(file, YAMLSeparator) {
|
||||
file += YAMLSeparator
|
||||
}
|
||||
builder.WriteString(file)
|
||||
}
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
func (l *LocalFileRenderer) SetVersion(version string) {
|
||||
l.Opts.Version = version
|
||||
}
|
||||
|
||||
// LocalChartRenderer load chart from local file system
|
||||
type LocalChartRenderer struct {
|
||||
Opts *RendererOptions
|
||||
Chart *chart.Chart
|
||||
Started bool
|
||||
}
|
||||
|
||||
func (lr *LocalRenderer) Init() error {
|
||||
func (lr *LocalChartRenderer) Init() error {
|
||||
fileNames, err := getFileNames(lr.Opts.FS, lr.Opts.Dir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
@@ -212,18 +282,18 @@ func (lr *LocalRenderer) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lr *LocalRenderer) RenderManifest(valsYaml string) (string, error) {
|
||||
func (lr *LocalChartRenderer) RenderManifest(valsYaml string) (string, error) {
|
||||
if !lr.Started {
|
||||
return "", errors.New("LocalRenderer has not been init")
|
||||
return "", errors.New("LocalChartRenderer has not been init")
|
||||
}
|
||||
return renderManifest(valsYaml, lr.Chart, true, lr.Opts, DefaultFilters...)
|
||||
}
|
||||
|
||||
func (lr *LocalRenderer) SetVersion(version string) {
|
||||
func (lr *LocalChartRenderer) SetVersion(version string) {
|
||||
lr.Opts.Version = version
|
||||
}
|
||||
|
||||
func NewLocalRenderer(opts ...RendererOption) (Renderer, error) {
|
||||
func NewLocalChartRenderer(opts ...RendererOption) (Renderer, error) {
|
||||
newOpts := &RendererOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(newOpts)
|
||||
@@ -232,7 +302,7 @@ func NewLocalRenderer(opts ...RendererOption) (Renderer, error) {
|
||||
if err := verifyRendererOptions(newOpts); err != nil {
|
||||
return nil, fmt.Errorf("verify err: %s", err)
|
||||
}
|
||||
return &LocalRenderer{
|
||||
return &LocalChartRenderer{
|
||||
Opts: newOpts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/installer"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
|
||||
"github.com/alibaba/higress/pkg/cmd/options"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -32,13 +31,16 @@ const (
|
||||
// manifestsFlagHelpStr is the command line description for --manifests
|
||||
manifestsFlagHelpStr = `Specify a path to a directory of profiles
|
||||
(e.g. ~/Downloads/higress/manifests).`
|
||||
outputHelpstr = "Specify a file to write profile yaml"
|
||||
filenameFlagHelpStr = "Path to file containing helm custom values"
|
||||
outputHelpstr = "Specify a file to write profile yaml"
|
||||
|
||||
profileNameK8s = "k8s"
|
||||
profileNameLocalK8s = "local-k8s"
|
||||
profileNameK8s = "k8s"
|
||||
profileNameLocalK8s = "local-k8s"
|
||||
profileNameLocalDocker = "local-docker"
|
||||
)
|
||||
|
||||
type InstallArgs struct {
|
||||
// InFilenames is a filename to helm custom values
|
||||
InFilenames []string
|
||||
// KubeConfigPath is the path to kube config file.
|
||||
KubeConfigPath string
|
||||
@@ -61,6 +63,7 @@ func (a *InstallArgs) String() string {
|
||||
}
|
||||
|
||||
func addInstallFlags(cmd *cobra.Command, args *InstallArgs) {
|
||||
cmd.PersistentFlags().StringSliceVarP(&args.InFilenames, "filename", "f", nil, filenameFlagHelpStr)
|
||||
cmd.PersistentFlags().StringArrayVarP(&args.Set, "set", "s", nil, setFlagHelpStr)
|
||||
cmd.PersistentFlags().StringVarP(&args.ManifestsPath, "manifests", "d", "", manifestsFlagHelpStr)
|
||||
}
|
||||
@@ -87,19 +90,23 @@ func newInstallCmd() *cobra.Command {
|
||||
# Install higress on local kubernetes cluster
|
||||
hgctl install --set profile=local-k8s
|
||||
|
||||
# Install higress on local docker environment with specific gateway port
|
||||
hgctl install --set profile=local-docker --set gateway.httpPort=80 --set gateway.httpsPort=443
|
||||
|
||||
# To override profile setting
|
||||
hgctl install --set profile=local-k8s --set global.enableIstioAPI=true --set gateway.replicas=2"
|
||||
|
||||
# To override helm setting
|
||||
hgctl install --set profile=local-k8s --set values.global.proxy.resources.requsts.cpu=500m"
|
||||
|
||||
|
||||
`,
|
||||
Args: cobra.ExactArgs(0),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return Install(cmd.OutOrStdout(), iArgs)
|
||||
return install(cmd.OutOrStdout(), iArgs)
|
||||
},
|
||||
}
|
||||
addInstallFlags(installCmd, iArgs)
|
||||
@@ -108,7 +115,7 @@ func newInstallCmd() *cobra.Command {
|
||||
return installCmd
|
||||
}
|
||||
|
||||
func Install(writer io.Writer, iArgs *InstallArgs) error {
|
||||
func install(writer io.Writer, iArgs *InstallArgs) error {
|
||||
setFlags := applyFlagAliases(iArgs.Set, iArgs.ManifestsPath)
|
||||
|
||||
// check profileName
|
||||
@@ -133,7 +140,7 @@ func Install(writer io.Writer, iArgs *InstallArgs) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = InstallManifests(profile, writer)
|
||||
err = installManifests(profile, writer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to install manifests: %v", err)
|
||||
}
|
||||
@@ -158,11 +165,12 @@ func promptInstall(writer io.Writer, profileName string) bool {
|
||||
|
||||
func promptProfileName(writer io.Writer) string {
|
||||
answer := ""
|
||||
fmt.Fprintf(writer, "Please select higress install configration profile:\n")
|
||||
fmt.Fprintf(writer, "1.Install higress to local kubernetes cluster like kind etc.\n")
|
||||
fmt.Fprintf(writer, "2.Install higress to kubernetes cluster\n")
|
||||
fmt.Fprintf(writer, "\nPlease select higress install configration profile:\n")
|
||||
fmt.Fprintf(writer, "\n1.Install higress to local kubernetes cluster like kind etc.\n")
|
||||
fmt.Fprintf(writer, "\n2.Install higress to kubernetes cluster\n")
|
||||
fmt.Fprintf(writer, "\n3.Install higress to local docker environment\n")
|
||||
for {
|
||||
fmt.Fprintf(writer, "Please input 1 or 2 to select, input your selection:")
|
||||
fmt.Fprintf(writer, "\nPlease input 1, 2 or 3 to select, input your selection:")
|
||||
fmt.Scanln(&answer)
|
||||
if strings.TrimSpace(answer) == "1" {
|
||||
return profileNameLocalK8s
|
||||
@@ -170,33 +178,23 @@ func promptProfileName(writer io.Writer) string {
|
||||
if strings.TrimSpace(answer) == "2" {
|
||||
return profileNameK8s
|
||||
}
|
||||
if strings.TrimSpace(answer) == "3" {
|
||||
return profileNameLocalDocker
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func InstallManifests(profile *helm.Profile, writer io.Writer) error {
|
||||
cliClient, err := kubernetes.NewCLIClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build kubernetes client: %w", err)
|
||||
}
|
||||
|
||||
op, err := installer.NewInstaller(profile, cliClient, writer, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := op.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifestMap, err := op.RenderManifests()
|
||||
func installManifests(profile *helm.Profile, writer io.Writer) error {
|
||||
installer, err := installer.NewInstaller(profile, writer, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(writer, "\n⌛️ Processing installation... \n\n")
|
||||
if err := op.ApplyManifests(manifestMap); err != nil {
|
||||
err = installer.Install()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(writer, "\n🎊 Install All Resources Complete!\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -15,11 +15,10 @@
|
||||
package installer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -96,29 +95,19 @@ func NewHigressComponent(profile *helm.Profile, writer io.Writer, opts ...Compon
|
||||
opt(newOpts)
|
||||
}
|
||||
|
||||
var renderer helm.Renderer
|
||||
var err error
|
||||
if newOpts.RepoURL != "" {
|
||||
renderer, err = helm.NewRemoteRenderer(
|
||||
helm.WithName(newOpts.ChartName),
|
||||
helm.WithNamespace(newOpts.Namespace),
|
||||
helm.WithRepoURL(newOpts.RepoURL),
|
||||
helm.WithVersion(newOpts.Version),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
renderer, err = helm.NewLocalRenderer(
|
||||
helm.WithName(newOpts.ChartName),
|
||||
helm.WithNamespace(newOpts.Namespace),
|
||||
helm.WithVersion(newOpts.Version),
|
||||
helm.WithFS(os.DirFS(newOpts.ChartPath)),
|
||||
helm.WithDir(string(Higress)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(newOpts.RepoURL) == 0 {
|
||||
return nil, errors.New("Higress helm chart url can't be empty")
|
||||
}
|
||||
|
||||
// Higress can only be installed by remote type
|
||||
renderer, err := helm.NewRemoteRenderer(
|
||||
helm.WithName(newOpts.ChartName),
|
||||
helm.WithNamespace(newOpts.Namespace),
|
||||
helm.WithRepoURL(newOpts.RepoURL),
|
||||
helm.WithVersion(newOpts.Version),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
higressComponent := &HigressComponent{
|
||||
|
||||
@@ -18,196 +18,113 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/helm/object"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
|
||||
"github.com/alibaba/higress/pkg/cmd/options"
|
||||
"k8s.io/client-go/util/homedir"
|
||||
)
|
||||
|
||||
type Installer struct {
|
||||
started bool
|
||||
components map[ComponentName]Component
|
||||
kubeCli kubernetes.CLIClient
|
||||
profile *helm.Profile
|
||||
writer io.Writer
|
||||
const (
|
||||
HgctlHomeDirPath = ".hgctl"
|
||||
StandaloneInstalledPath = "higress-standalone"
|
||||
ProfileInstalledPath = "profiles"
|
||||
InstalledYamlFileName = "install.yaml"
|
||||
DefaultGatewayAPINamespace = "gateway-system"
|
||||
DefaultIstioNamespace = "istio-system"
|
||||
)
|
||||
|
||||
type Installer interface {
|
||||
Install() error
|
||||
UnInstall() error
|
||||
Upgrade() error
|
||||
}
|
||||
|
||||
// Run must be invoked before invoking other functions.
|
||||
func (o *Installer) Run() error {
|
||||
for name, component := range o.components {
|
||||
if !component.Enabled() {
|
||||
continue
|
||||
}
|
||||
if err := component.Run(); err != nil {
|
||||
return fmt.Errorf("component %s run failed, err: %s", name, err)
|
||||
}
|
||||
}
|
||||
o.started = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// RenderManifests renders component manifests specified by profile.
|
||||
func (o *Installer) RenderManifests() (map[ComponentName]string, error) {
|
||||
if !o.started {
|
||||
return nil, errors.New("HigressOperator is not running")
|
||||
}
|
||||
res := make(map[ComponentName]string)
|
||||
for name, component := range o.components {
|
||||
if !component.Enabled() {
|
||||
continue
|
||||
}
|
||||
manifest, err := component.RenderManifest()
|
||||
func NewInstaller(profile *helm.Profile, writer io.Writer, quiet bool) (Installer, error) {
|
||||
switch profile.Global.Install {
|
||||
case helm.InstallK8s, helm.InstallLocalK8s:
|
||||
cliClient, err := kubernetes.NewCLIClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("component %s RenderManifest err: %v", name, err)
|
||||
return nil, fmt.Errorf("failed to build kubernetes client: %w", err)
|
||||
}
|
||||
res[name] = manifest
|
||||
installer, err := NewK8sInstaller(profile, cliClient, writer, quiet)
|
||||
return installer, err
|
||||
case helm.InstallLocalDocker:
|
||||
installer, err := NewDockerInstaller(profile, writer, quiet)
|
||||
return installer, err
|
||||
default:
|
||||
return nil, errors.New("install is not supported")
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// GenerateManifests generates component manifests to k8s cluster
|
||||
func (o *Installer) GenerateManifests(manifestMap map[ComponentName]string) error {
|
||||
if o.kubeCli == nil {
|
||||
return errors.New("no injected k8s cli into Installer")
|
||||
func GetHomeDir() (string, error) {
|
||||
home := homedir.HomeDir()
|
||||
if home == "" {
|
||||
return "", fmt.Errorf("No user home environment variable found for OS %s", runtime.GOOS)
|
||||
}
|
||||
for _, manifest := range manifestMap {
|
||||
fmt.Fprint(o.writer, manifest)
|
||||
}
|
||||
return nil
|
||||
|
||||
return home, nil
|
||||
}
|
||||
|
||||
// ApplyManifests apply component manifests to k8s cluster
|
||||
func (o *Installer) ApplyManifests(manifestMap map[ComponentName]string) error {
|
||||
if o.kubeCli == nil {
|
||||
return errors.New("no injected k8s cli into Installer")
|
||||
}
|
||||
for name, manifest := range manifestMap {
|
||||
namespace := o.components[name].Namespace()
|
||||
if err := o.applyManifest(manifest, namespace); err != nil {
|
||||
return fmt.Errorf("component %s ApplyManifest err: %v", name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Installer) applyManifest(manifest string, ns string) error {
|
||||
if err := o.kubeCli.CreateNamespace(ns); err != nil {
|
||||
return err
|
||||
}
|
||||
objs, err := object.ParseK8sObjectsFromYAMLManifest(manifest)
|
||||
func GetHgctlPath() (string, error) {
|
||||
home, err := GetHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
for _, obj := range objs {
|
||||
// check namespaced object if namespace property has been existed
|
||||
if obj.Namespace == "" && o.isNamespacedObject(obj) {
|
||||
obj.Namespace = ns
|
||||
obj.UnstructuredObject().SetNamespace(ns)
|
||||
}
|
||||
if o.isNamespacedObject(obj) {
|
||||
fmt.Fprintf(o.writer, "✔️ Installed %s:%s:%s.\n", obj.Kind, obj.Name, obj.Namespace)
|
||||
} else {
|
||||
fmt.Fprintf(o.writer, "✔️ Installed %s::%s.\n", obj.Kind, obj.Name)
|
||||
}
|
||||
if err := o.kubeCli.ApplyObject(obj.UnstructuredObject()); err != nil {
|
||||
return err
|
||||
|
||||
hgctlPath := filepath.Join(home, HgctlHomeDirPath)
|
||||
if _, err := os.Stat(hgctlPath); os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(hgctlPath, os.ModePerm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
return hgctlPath, nil
|
||||
}
|
||||
|
||||
// DeleteManifests delete component manifests to k8s cluster
|
||||
func (o *Installer) DeleteManifests(manifestMap map[ComponentName]string) error {
|
||||
if o.kubeCli == nil {
|
||||
return errors.New("no injected k8s cli into Installer")
|
||||
}
|
||||
for name, manifest := range manifestMap {
|
||||
namespace := o.components[name].Namespace()
|
||||
if err := o.deleteManifest(manifest, namespace); err != nil {
|
||||
return fmt.Errorf("component %s DeleteManifest err: %v", name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteManifest delete manifest to certain namespace
|
||||
func (o *Installer) deleteManifest(manifest string, ns string) error {
|
||||
objs, err := object.ParseK8sObjectsFromYAMLManifest(manifest)
|
||||
func GetDefaultInstallPackagePath() (string, error) {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
for _, obj := range objs {
|
||||
// check namespaced object if namespace property has been existed
|
||||
if obj.Namespace == "" && o.isNamespacedObject(obj) {
|
||||
obj.Namespace = ns
|
||||
obj.UnstructuredObject().SetNamespace(ns)
|
||||
}
|
||||
if o.isNamespacedObject(obj) {
|
||||
fmt.Fprintf(o.writer, "✔️ Removed %s:%s:%s.\n", obj.Kind, obj.Name, obj.Namespace)
|
||||
} else {
|
||||
fmt.Fprintf(o.writer, "✔️ Removed %s::%s.\n", obj.Kind, obj.Name)
|
||||
}
|
||||
if err := o.kubeCli.DeleteObject(obj.UnstructuredObject()); err != nil {
|
||||
return err
|
||||
|
||||
path := filepath.Join(dir, StandaloneInstalledPath)
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(path, os.ModePerm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return path, err
|
||||
}
|
||||
|
||||
func (o *Installer) isNamespacedObject(obj *object.K8sObject) bool {
|
||||
if obj.Kind != "CustomResourceDefinition" && obj.Kind != "ClusterRole" && obj.Kind != "ClusterRoleBinding" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func NewInstaller(profile *helm.Profile, cli kubernetes.CLIClient, writer io.Writer, quiet bool) (*Installer, error) {
|
||||
if profile == nil {
|
||||
return nil, errors.New("install profile is empty")
|
||||
}
|
||||
// initialize components
|
||||
components := make(map[ComponentName]Component)
|
||||
opts := []ComponentOption{
|
||||
WithComponentNamespace(profile.Global.Namespace),
|
||||
WithComponentChartPath(profile.InstallPackagePath),
|
||||
WithComponentVersion(profile.Charts.Higress.Version),
|
||||
WithComponentRepoURL(profile.Charts.Higress.Url),
|
||||
WithComponentChartName(profile.Charts.Higress.Name),
|
||||
}
|
||||
if quiet {
|
||||
opts = append(opts, WithQuiet())
|
||||
}
|
||||
higressComponent, err := NewHigressComponent(profile, writer, opts...)
|
||||
func GetProfileInstalledPath() (string, error) {
|
||||
hgctlPath, err := GetHgctlPath()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("NewHigressComponent failed, err: %s", err)
|
||||
return "", err
|
||||
}
|
||||
components[Higress] = higressComponent
|
||||
|
||||
if profile.IstioEnabled() {
|
||||
opts := []ComponentOption{
|
||||
WithComponentNamespace(profile.Global.IstioNamespace),
|
||||
WithComponentChartPath(profile.InstallPackagePath),
|
||||
WithComponentVersion(profile.Charts.Istio.Version),
|
||||
WithComponentRepoURL(profile.Charts.Istio.Url),
|
||||
WithComponentChartName(profile.Charts.Istio.Name),
|
||||
}
|
||||
if quiet {
|
||||
opts = append(opts, WithQuiet())
|
||||
profilesPath := filepath.Join(hgctlPath, ProfileInstalledPath)
|
||||
if _, err := os.Stat(profilesPath); os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(profilesPath, os.ModePerm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
istioCRDComponent, err := NewIstioCRDComponent(profile, writer, opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("NewIstioCRDComponent failed, err: %s", err)
|
||||
}
|
||||
components[Istio] = istioCRDComponent
|
||||
}
|
||||
op := &Installer{
|
||||
profile: profile,
|
||||
components: components,
|
||||
kubeCli: cli,
|
||||
writer: writer,
|
||||
}
|
||||
return op, nil
|
||||
return profilesPath, nil
|
||||
}
|
||||
|
||||
func GetInstalledYamlPath() (string, bool) {
|
||||
profileInstalledPath, err := GetProfileInstalledPath()
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
installedYamlFile := filepath.Join(profileInstalledPath, InstalledYamlFileName)
|
||||
if _, err := os.Stat(installedYamlFile); os.IsNotExist(err) {
|
||||
return installedYamlFile, false
|
||||
}
|
||||
return installedYamlFile, true
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -17,9 +17,10 @@ package installer
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/manifests"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -42,23 +43,27 @@ func NewIstioCRDComponent(profile *helm.Profile, writer io.Writer, opts ...Compo
|
||||
|
||||
var renderer helm.Renderer
|
||||
var err error
|
||||
if newOpts.RepoURL != "" {
|
||||
renderer, err = helm.NewRemoteRenderer(
|
||||
|
||||
// Istio can be installed by embed type or remote type
|
||||
if strings.HasPrefix(newOpts.RepoURL, "embed://") {
|
||||
chartDir := strings.TrimPrefix(newOpts.RepoURL, "embed://")
|
||||
renderer, err = helm.NewLocalChartRenderer(
|
||||
helm.WithName(newOpts.ChartName),
|
||||
helm.WithNamespace(newOpts.Namespace),
|
||||
helm.WithRepoURL(newOpts.RepoURL),
|
||||
helm.WithVersion(newOpts.Version),
|
||||
helm.WithFS(manifests.BuiltinOrDir("")),
|
||||
helm.WithDir(chartDir),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
renderer, err = helm.NewLocalRenderer(
|
||||
renderer, err = helm.NewRemoteRenderer(
|
||||
helm.WithName(newOpts.ChartName),
|
||||
helm.WithNamespace(newOpts.Namespace),
|
||||
helm.WithRepoURL(newOpts.RepoURL),
|
||||
helm.WithVersion(newOpts.Version),
|
||||
helm.WithFS(os.DirFS(newOpts.ChartPath)),
|
||||
helm.WithDir(string(Istio)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
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
|
||||
}
|
||||
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{})
|
||||
}
|
||||
@@ -59,12 +59,15 @@ func newManifestCmd() *cobra.Command {
|
||||
|
||||
generate := newManifestGenerateCmd(iArgs)
|
||||
addManifestFlags(generate, iArgs)
|
||||
flags := generate.Flags()
|
||||
options.AddKubeConfigFlags(flags)
|
||||
manifestCmd.AddCommand(generate)
|
||||
|
||||
return manifestCmd
|
||||
}
|
||||
|
||||
func addManifestFlags(cmd *cobra.Command, args *ManifestArgs) {
|
||||
cmd.PersistentFlags().StringSliceVarP(&args.InFilenames, "filename", "f", nil, filenameFlagHelpStr)
|
||||
cmd.PersistentFlags().StringArrayVarP(&args.Set, "set", "s", nil, setFlagHelpStr)
|
||||
cmd.PersistentFlags().StringVarP(&args.ManifestsPath, "manifests", "d", "", manifestsFlagHelpStr)
|
||||
}
|
||||
@@ -123,7 +126,7 @@ func genManifests(profile *helm.Profile, writer io.Writer) error {
|
||||
return fmt.Errorf("failed to build kubernetes client: %w", err)
|
||||
}
|
||||
|
||||
op, err := installer.NewInstaller(profile, cliClient, writer, true)
|
||||
op, err := installer.NewK8sInstaller(profile, cliClient, writer, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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"
|
||||
@@ -23,6 +23,8 @@ import (
|
||||
// FS embeds the manifests
|
||||
//
|
||||
//go:embed profiles/*
|
||||
//go:embed gatewayapi/*
|
||||
//go:embed istiobase/*
|
||||
var FS embed.FS
|
||||
|
||||
// BuiltinOrDir returns a FS for the provided directory. If no directory is passed, the compiled in
|
||||
|
||||
@@ -1,38 +1,15 @@
|
||||
# Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
profile: kind
|
||||
profile: all
|
||||
global:
|
||||
install: local # install mode k8s/local/docker
|
||||
install: k8s # install mode k8s/local-k8s/local-docker/local
|
||||
ingressClass: higress
|
||||
watchNamespace:
|
||||
disableAlpnH2: true
|
||||
enableStatus: true
|
||||
enableIstioAPI: true
|
||||
enableGatewayAPI: false
|
||||
namespace: higress-system
|
||||
istioNamespace: istio-system
|
||||
|
||||
console:
|
||||
port: 8080
|
||||
replicas: 1
|
||||
serviceType: ClusterIP
|
||||
domain: console.higress.io
|
||||
tlsSecretName:
|
||||
webLoginPrompt:
|
||||
adminPasswordValue: admin
|
||||
adminPasswordLength: 8
|
||||
o11yEnabled: false
|
||||
pvcRwxSupported: true
|
||||
|
||||
gateway:
|
||||
replicas: 1
|
||||
@@ -44,7 +21,7 @@ controller:
|
||||
replicas: 1
|
||||
|
||||
storage:
|
||||
url: nacos://192.168.0.1:8848 # file://opt/higress/conf, buildin://127.0.0.1:8848
|
||||
url: nacos://127.0.0.1:8848 # file://opt/higress/conf
|
||||
ns: higress-system
|
||||
username:
|
||||
password:
|
||||
@@ -58,8 +35,8 @@ charts:
|
||||
higress:
|
||||
url: https://higress.io/helm-charts
|
||||
name: higress
|
||||
version: 1.1.2
|
||||
istio:
|
||||
url: https://istio-release.storage.googleapis.com/charts
|
||||
name: base
|
||||
version: 1.18.2
|
||||
version: latest
|
||||
standalone:
|
||||
url: https://higress.io/standalone/get-higress.sh
|
||||
name: standalone
|
||||
version: latest
|
||||
@@ -1,37 +1,14 @@
|
||||
# Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
profile: k8s
|
||||
global:
|
||||
install: k8s # install mode k8s/local-k8s/local-docker/local
|
||||
ingressClass: higress
|
||||
watchNamespace:
|
||||
disableAlpnH2: true
|
||||
enableStatus: true
|
||||
enableIstioAPI: false
|
||||
enableGatewayAPI: false
|
||||
namespace: higress-system
|
||||
istioNamespace: istio-system
|
||||
|
||||
console:
|
||||
replicas: 1
|
||||
serviceType: ClusterIP
|
||||
domain: console.higress.io
|
||||
tlsSecretName:
|
||||
webLoginPrompt:
|
||||
adminPasswordValue: admin
|
||||
adminPasswordLength: 8
|
||||
o11yEnabled: false
|
||||
pvcRwxSupported: true
|
||||
|
||||
gateway:
|
||||
replicas: 2
|
||||
@@ -46,8 +23,8 @@ charts:
|
||||
higress:
|
||||
url: https://higress.io/helm-charts
|
||||
name: higress
|
||||
version: 1.1.2
|
||||
istio:
|
||||
url: https://istio-release.storage.googleapis.com/charts
|
||||
name: base
|
||||
version: 1.18.2
|
||||
version: latest
|
||||
standalone:
|
||||
url: https://higress.io/standalone/get-higress.sh
|
||||
name: standalone
|
||||
version: latest
|
||||
|
||||
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
|
||||
|
||||
@@ -1,37 +1,14 @@
|
||||
# Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
profile: local-k8s
|
||||
global:
|
||||
install: local-k8s # install mode k8s/local-k8s/local-docker/local
|
||||
ingressClass: higress
|
||||
watchNamespace:
|
||||
disableAlpnH2: true
|
||||
enableStatus: true
|
||||
enableIstioAPI: true
|
||||
enableGatewayAPI: true
|
||||
namespace: higress-system
|
||||
istioNamespace: istio-system
|
||||
|
||||
console:
|
||||
replicas: 1
|
||||
serviceType: ClusterIP
|
||||
domain: console.higress.io
|
||||
tlsSecretName:
|
||||
webLoginPrompt:
|
||||
adminPasswordValue: admin
|
||||
adminPasswordLength: 8
|
||||
o11yEnabled: true
|
||||
pvcRwxSupported: true
|
||||
|
||||
gateway:
|
||||
replicas: 1
|
||||
@@ -46,8 +23,8 @@ charts:
|
||||
higress:
|
||||
url: https://higress.io/helm-charts
|
||||
name: higress
|
||||
version: 1.1.2
|
||||
istio:
|
||||
url: https://istio-release.storage.googleapis.com/charts
|
||||
name: base
|
||||
version: 1.18.2
|
||||
version: latest
|
||||
standalone:
|
||||
url: https://higress.io/standalone/get-higress.sh
|
||||
name: standalone
|
||||
version: latest
|
||||
|
||||
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
|
||||
}
|
||||
64
pkg/cmd/hgctl/plugin/test/ls.go
Normal file
64
pkg/cmd/hgctl/plugin/test/ls.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/docker"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
func newLsCommand() *cobra.Command {
|
||||
lsCmd := &cobra.Command{
|
||||
Use: "ls",
|
||||
Aliases: []string{"l"},
|
||||
Short: "List all test environments",
|
||||
Example: ` hgctl plugin test ls`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(runLs(cmd.OutOrStdout()))
|
||||
},
|
||||
}
|
||||
|
||||
return lsCmd
|
||||
}
|
||||
|
||||
func runLs(w io.Writer) error {
|
||||
cli, err := docker.NewCompose(w)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to build the docker compose client")
|
||||
}
|
||||
|
||||
list, err := cli.List(context.TODO())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to list all test environments")
|
||||
}
|
||||
|
||||
printer := printers.GetNewTabWriter(w)
|
||||
// fmt.Fprintf(printer, "NAME\tSTATUS\tCONFIG FILES\n") // compose v2.3.0+
|
||||
fmt.Fprintf(printer, "NAME\tSTATUS\n")
|
||||
for _, stack := range list {
|
||||
fmt.Fprintf(printer, "%s\t%s\n", stack.Name, stack.Status)
|
||||
}
|
||||
printer.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
115
pkg/cmd/hgctl/plugin/test/start.go
Normal file
115
pkg/cmd/hgctl/plugin/test/start.go
Normal file
@@ -0,0 +1,115 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/docker"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/option"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
// TODO(WeixinX): If no test environment exists, create one first and then start
|
||||
type starter struct {
|
||||
optionFile string
|
||||
option.TestOptions
|
||||
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func newStartCommand() *cobra.Command {
|
||||
var s starter
|
||||
v := viper.New()
|
||||
|
||||
startCmd := &cobra.Command{
|
||||
Use: "start",
|
||||
Aliases: []string{"s"},
|
||||
Short: "Start the test environment",
|
||||
Example: ` # If the option.yaml file exists in the current path, do the following:
|
||||
hgctl plugin test start
|
||||
|
||||
# Run containers in the background with the option --detach(-d)
|
||||
hgctl plugin test start -d
|
||||
`,
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(s.config(v, cmd))
|
||||
},
|
||||
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(s.start())
|
||||
},
|
||||
}
|
||||
|
||||
flags := startCmd.PersistentFlags()
|
||||
option.AddOptionFileFlag(&s.optionFile, flags)
|
||||
v.BindPFlags(flags)
|
||||
|
||||
flags.StringP("name", "p", "wasm-test", "Test environment name")
|
||||
v.BindPFlag("test.name", flags.Lookup("name"))
|
||||
v.SetDefault("test.name", "wasm-test")
|
||||
|
||||
flags.StringP("test-path", "t", "./test", "Test configuration source")
|
||||
v.BindPFlag("test.test-path", flags.Lookup("test-path"))
|
||||
v.SetDefault("test.test-path", "./test")
|
||||
|
||||
flags.StringP("compose-file", "c", "", "Docker compose configuration file")
|
||||
v.BindPFlag("test.compose-file", flags.Lookup("compose-file"))
|
||||
v.SetDefault("test.compose-file", "")
|
||||
|
||||
flags.BoolP("detach", "d", false, "Detached mode: Run containers in the background")
|
||||
v.BindPFlag("test.detach", flags.Lookup("detach"))
|
||||
v.SetDefault("test.detach", false)
|
||||
|
||||
return startCmd
|
||||
}
|
||||
|
||||
func (s *starter) config(v *viper.Viper, cmd *cobra.Command) error {
|
||||
allOpt, err := option.ParseOptions(s.optionFile, v, cmd.PersistentFlags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.TestOptions = allOpt.Test
|
||||
|
||||
s.w = cmd.OutOrStdout()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *starter) start() error {
|
||||
cli, err := docker.NewCompose(s.w)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to build the docker compose client")
|
||||
}
|
||||
|
||||
var configs []string
|
||||
if s.ComposeFile != "" {
|
||||
configs = []string{s.ComposeFile}
|
||||
}
|
||||
|
||||
err = cli.Up(context.TODO(), s.Name, configs, s.TestPath, s.Detach)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to start the test environment")
|
||||
}
|
||||
fmt.Fprintf(s.w, "Started the test environment %q\n", s.Name)
|
||||
|
||||
return nil
|
||||
}
|
||||
95
pkg/cmd/hgctl/plugin/test/stop.go
Normal file
95
pkg/cmd/hgctl/plugin/test/stop.go
Normal file
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/docker"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/option"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
type stopper struct {
|
||||
optionFile string
|
||||
option.TestOptions
|
||||
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func newStopCommand() *cobra.Command {
|
||||
var s stopper
|
||||
v := viper.New()
|
||||
|
||||
stopCmd := &cobra.Command{
|
||||
Use: "stop",
|
||||
Aliases: []string{"st"},
|
||||
Short: "Stop the test environment",
|
||||
Example: ` # Stop responding to the compose containers with the option --name(-p)
|
||||
hgctl plugin test stop -p wasm-test
|
||||
`,
|
||||
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(s.config(v, cmd))
|
||||
},
|
||||
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(s.stop())
|
||||
},
|
||||
}
|
||||
|
||||
flags := stopCmd.PersistentFlags()
|
||||
option.AddOptionFileFlag(&s.optionFile, flags)
|
||||
v.BindPFlags(flags)
|
||||
|
||||
flags.StringP("name", "p", "wasm-test", "Test environment name")
|
||||
v.BindPFlag("test.name", flags.Lookup("name"))
|
||||
v.SetDefault("test.name", "wasm-test")
|
||||
|
||||
return stopCmd
|
||||
}
|
||||
|
||||
func (s *stopper) config(v *viper.Viper, cmd *cobra.Command) error {
|
||||
allOpt, err := option.ParseOptions(s.optionFile, v, cmd.PersistentFlags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.TestOptions = allOpt.Test
|
||||
|
||||
s.w = cmd.OutOrStdout()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *stopper) stop() error {
|
||||
cli, err := docker.NewCompose(s.w)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to build the docker compose client")
|
||||
}
|
||||
|
||||
err = cli.Down(context.TODO(), s.Name)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to stop the test environment %q", s.Name)
|
||||
}
|
||||
fmt.Fprintf(s.w, "Stopped the test environment %q\n", s.Name)
|
||||
|
||||
return nil
|
||||
}
|
||||
167
pkg/cmd/hgctl/plugin/test/templates.go
Normal file
167
pkg/cmd/hgctl/plugin/test/templates.go
Normal file
@@ -0,0 +1,167 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
const (
|
||||
dockerComposeYAML = `# File generated by hgctl. Modify as required.
|
||||
|
||||
version: '3.7'
|
||||
services:
|
||||
envoy:
|
||||
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/envoy:1.20
|
||||
command: envoy -c /etc/envoy/envoy.yaml --component-log-level wasm:debug
|
||||
depends_on:
|
||||
- httpbin
|
||||
networks:
|
||||
- wasmtest
|
||||
ports:
|
||||
- "10000:10000"
|
||||
volumes:
|
||||
- {{ .TestPath }}/envoy.yaml:/etc/envoy/envoy.yaml
|
||||
- {{ .ProductPath }}/plugin.wasm:/etc/envoy/plugin.wasm
|
||||
|
||||
httpbin:
|
||||
image: kennethreitz/httpbin:latest
|
||||
networks:
|
||||
- wasmtest
|
||||
ports:
|
||||
- "12345:80"
|
||||
|
||||
networks:
|
||||
wasmtest: {}
|
||||
`
|
||||
|
||||
envoyYAML = `# File generated by hgctl. Modify as required.
|
||||
|
||||
admin:
|
||||
address:
|
||||
socket_address:
|
||||
protocol: TCP
|
||||
address: 0.0.0.0
|
||||
port_value: 9901
|
||||
static_resources:
|
||||
listeners:
|
||||
- name: listener_0
|
||||
address:
|
||||
socket_address:
|
||||
protocol: TCP
|
||||
address: 0.0.0.0
|
||||
port_value: 10000
|
||||
filter_chains:
|
||||
- filters:
|
||||
- name: envoy.filters.network.http_connection_manager
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
|
||||
scheme_header_transformation:
|
||||
scheme_to_overwrite: https
|
||||
stat_prefix: ingress_http
|
||||
# Output envoy logs to stdout
|
||||
access_log:
|
||||
- name: envoy.access_loggers.stdout
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
|
||||
# Modify as required
|
||||
route_config:
|
||||
name: local_route
|
||||
virtual_hosts:
|
||||
- name: local_service
|
||||
domains: ["*"]
|
||||
routes:
|
||||
- match:
|
||||
prefix: "/"
|
||||
route:
|
||||
cluster: httpbin
|
||||
http_filters:
|
||||
- name: wasmtest
|
||||
typed_config:
|
||||
"@type": type.googleapis.com/udpa.type.v1.TypedStruct
|
||||
type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
|
||||
value:
|
||||
config:
|
||||
name: wasmtest
|
||||
vm_config:
|
||||
runtime: envoy.wasm.runtime.v8
|
||||
code:
|
||||
local:
|
||||
filename: /etc/envoy/plugin.wasm
|
||||
configuration:
|
||||
"@type": "type.googleapis.com/google.protobuf.StringValue"
|
||||
# Modify as required
|
||||
value: |
|
||||
{{ .JSONExample }}
|
||||
- name: envoy.filters.http.router
|
||||
clusters:
|
||||
- name: httpbin
|
||||
connect_timeout: 30s
|
||||
type: LOGICAL_DNS
|
||||
# Comment out the following line to test on v6 networks
|
||||
dns_lookup_family: V4_ONLY
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: httpbin
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: httpbin
|
||||
port_value: 80
|
||||
`
|
||||
)
|
||||
|
||||
type DockerCompose struct {
|
||||
TestPath string
|
||||
ProductPath string
|
||||
}
|
||||
|
||||
type Envoy struct {
|
||||
JSONExample string
|
||||
}
|
||||
|
||||
func genDockerComposeYAML(d *DockerCompose, dir string) error {
|
||||
path := fmt.Sprintf("%s/docker-compose.yaml", dir)
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err = template.Must(template.New("DockerComposeYAML").Parse(dockerComposeYAML)).Execute(f, d); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func genEnvoyYAML(e *Envoy, dir string) error {
|
||||
path := fmt.Sprintf("%s/envoy.yaml", dir)
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to create %q: %v\n", path, err))
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err = template.Must(template.New("EnvoyYAML").Parse(envoyYAML)).Execute(f, e); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
35
pkg/cmd/hgctl/plugin/test/test.go
Normal file
35
pkg/cmd/hgctl/plugin/test/test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewCommand() *cobra.Command {
|
||||
testCmd := &cobra.Command{
|
||||
Use: "test",
|
||||
Aliases: []string{"t"},
|
||||
Short: "Test WASM plugin locally",
|
||||
}
|
||||
|
||||
testCmd.AddCommand(newCreateCommand())
|
||||
testCmd.AddCommand(newStartCommand())
|
||||
testCmd.AddCommand(newStopCommand())
|
||||
testCmd.AddCommand(newCleanCommand())
|
||||
testCmd.AddCommand(newLsCommand())
|
||||
|
||||
return testCmd
|
||||
}
|
||||
163
pkg/cmd/hgctl/plugin/types/annotation.go
Normal file
163
pkg/cmd/hgctl/plugin/types/annotation.go
Normal file
@@ -0,0 +1,163 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Annotation struct {
|
||||
Type AnnotationType
|
||||
I18nType I18nType
|
||||
Text string
|
||||
}
|
||||
|
||||
type AnnotationType int
|
||||
|
||||
const (
|
||||
// Info
|
||||
ACategory AnnotationType = iota
|
||||
AName
|
||||
ATitle
|
||||
ADescription
|
||||
AIconUrl
|
||||
AVersion
|
||||
AContactName
|
||||
AContactUrl
|
||||
AContactEmail
|
||||
|
||||
// Spec
|
||||
APhase
|
||||
APriority
|
||||
|
||||
// Schema
|
||||
AScope
|
||||
AExample
|
||||
AEnd
|
||||
|
||||
AUnknown
|
||||
)
|
||||
|
||||
func str2AnnotationType(typ string) AnnotationType {
|
||||
switch strings.ToLower(typ) {
|
||||
case "@category":
|
||||
return ACategory
|
||||
case "@name":
|
||||
return AName
|
||||
case "@title":
|
||||
return ATitle
|
||||
case "@description":
|
||||
return ADescription
|
||||
case "@iconurl":
|
||||
return AIconUrl
|
||||
case "@version":
|
||||
return AVersion
|
||||
case "@contact.name":
|
||||
return AContactName
|
||||
case "@contact.url":
|
||||
return AContactUrl
|
||||
case "@contact.email":
|
||||
return AContactEmail
|
||||
case "@phase":
|
||||
return APhase
|
||||
case "@priority":
|
||||
return APriority
|
||||
case "@scope":
|
||||
return AScope
|
||||
case "@example":
|
||||
return AExample
|
||||
case "@end":
|
||||
return AEnd
|
||||
default:
|
||||
return AUnknown
|
||||
}
|
||||
}
|
||||
|
||||
// GetAnnotations returns all annotations in the comment
|
||||
func GetAnnotations(comment string) []Annotation {
|
||||
as := make([]Annotation, 0)
|
||||
cs := strings.Split(comment, "\n")
|
||||
for i := 0; i < len(cs); i++ {
|
||||
a, err := getAnnotationFrom(cs[i])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if a.Type == AExample {
|
||||
for j := i + 1; j < len(cs); j++ {
|
||||
if str2AnnotationType(strings.TrimSpace(cs[j])) == AEnd {
|
||||
break
|
||||
}
|
||||
if j == i+1 {
|
||||
a.Text = fmt.Sprintf("%s", cs[j])
|
||||
} else {
|
||||
a.Text = fmt.Sprintf("%s\n%s", a.Text, cs[j])
|
||||
}
|
||||
}
|
||||
}
|
||||
as = append(as, a)
|
||||
}
|
||||
return as
|
||||
}
|
||||
|
||||
func getAnnotationFrom(c string) (Annotation, error) {
|
||||
// the annotation is like `@AnnotationType [I18nType] Text`
|
||||
|
||||
c = strings.TrimSpace(c)
|
||||
if !strings.HasPrefix(c, "@") {
|
||||
return Annotation{}, errors.New("invalid annotation")
|
||||
}
|
||||
|
||||
// first param: AnnotationType
|
||||
idx := strings.Index(c, " ")
|
||||
if idx == -1 && str2AnnotationType(c) == AUnknown { // only an invalid annotation type
|
||||
return Annotation{}, errors.New("invalid annotation")
|
||||
}
|
||||
// idx != -1 or type != unknown
|
||||
var typ AnnotationType
|
||||
if idx == -1 {
|
||||
typ = str2AnnotationType(c)
|
||||
} else {
|
||||
typ = str2AnnotationType(strings.TrimSpace(c[0:idx]))
|
||||
}
|
||||
c = strings.TrimSpace(c[idx+1:])
|
||||
a := Annotation{
|
||||
Type: typ,
|
||||
I18nType: I18nDefault,
|
||||
Text: c,
|
||||
}
|
||||
if a.Type != ATitle && a.Type != ADescription { // other annotation types do not define i18n
|
||||
a.I18nType = I18nUndefined
|
||||
}
|
||||
if idx == -1 && typ != AUnknown { // only a valid annotation type
|
||||
a.Text = ""
|
||||
}
|
||||
|
||||
// second or/and third param: I18nType and Text
|
||||
idx = strings.Index(c, " ")
|
||||
if idx == -1 {
|
||||
return a, nil
|
||||
}
|
||||
i18n := str2I18nType(strings.TrimSpace(c[0:idx]))
|
||||
if i18n == I18nUnknown {
|
||||
return a, nil
|
||||
}
|
||||
a.I18nType = i18n
|
||||
a.Text = strings.TrimSpace(c[idx+1:])
|
||||
return a, nil
|
||||
}
|
||||
176
pkg/cmd/hgctl/plugin/types/marshal.go
Normal file
176
pkg/cmd/hgctl/plugin/types/marshal.go
Normal file
@@ -0,0 +1,176 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v3"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
)
|
||||
|
||||
func (s JSON) MarshalJSON() ([]byte, error) {
|
||||
if len(s.Raw) > 0 {
|
||||
var obj interface{}
|
||||
err := json.Unmarshal(s.Raw, &obj)
|
||||
if err != nil {
|
||||
return []byte("null"), err
|
||||
}
|
||||
return json.Marshal(obj)
|
||||
}
|
||||
return []byte("null"), nil
|
||||
}
|
||||
|
||||
func (s *JSON) UnmarshalJSON(data []byte) error {
|
||||
if len(data) > 0 && !bytes.Equal(data, []byte("null")) {
|
||||
s.Raw = data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s JSON) MarshalYAML() (interface{}, error) {
|
||||
if len(s.Raw) > 0 {
|
||||
var obj interface{}
|
||||
err := yaml.Unmarshal(s.Raw, &obj)
|
||||
if err != nil {
|
||||
return "null", err
|
||||
}
|
||||
return obj, nil
|
||||
}
|
||||
return "null", nil
|
||||
}
|
||||
|
||||
func (s JSONSchemaPropsOrArray) MarshalJSON() ([]byte, error) {
|
||||
if len(s.JSONSchemas) > 0 {
|
||||
return json.Marshal(s.JSONSchemas)
|
||||
}
|
||||
return json.Marshal(s.Schema)
|
||||
}
|
||||
|
||||
func (s *JSONSchemaPropsOrArray) UnmarshalJSON(data []byte) error {
|
||||
var nw JSONSchemaPropsOrArray
|
||||
var first byte
|
||||
if len(data) > 1 {
|
||||
first = data[0]
|
||||
}
|
||||
if first == '{' {
|
||||
var sch JSONSchemaProps
|
||||
if err := json.Unmarshal(data, &sch); err != nil {
|
||||
return err
|
||||
}
|
||||
nw.Schema = &sch
|
||||
}
|
||||
if first == '[' {
|
||||
if err := json.Unmarshal(data, &nw.JSONSchemas); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
*s = nw
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s JSONSchemaPropsOrArray) MarshalYAML() (interface{}, error) {
|
||||
if len(s.JSONSchemas) > 0 {
|
||||
return s.JSONSchemas, nil
|
||||
}
|
||||
return s.Schema, nil
|
||||
}
|
||||
|
||||
func (s JSONSchemaPropsOrBool) MarshalJSON() ([]byte, error) {
|
||||
if s.Schema != nil {
|
||||
return json.Marshal(s.Schema)
|
||||
}
|
||||
|
||||
if s.Schema == nil && !s.Allows {
|
||||
return []byte("false"), nil
|
||||
}
|
||||
return []byte("true"), nil
|
||||
}
|
||||
|
||||
func (s *JSONSchemaPropsOrBool) UnmarshalJSON(data []byte) error {
|
||||
var nw JSONSchemaPropsOrBool
|
||||
switch {
|
||||
case len(data) == 0:
|
||||
case data[0] == '{':
|
||||
var sch JSONSchemaProps
|
||||
if err := json.Unmarshal(data, &sch); err != nil {
|
||||
return err
|
||||
}
|
||||
nw.Allows = true
|
||||
nw.Schema = &sch
|
||||
case len(data) == 4 && string(data) == "true":
|
||||
nw.Allows = true
|
||||
case len(data) == 5 && string(data) == "false":
|
||||
nw.Allows = false
|
||||
default:
|
||||
return errors.New("boolean or JSON schema expected")
|
||||
}
|
||||
*s = nw
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s JSONSchemaPropsOrBool) MarshalYAML() (interface{}, error) {
|
||||
if s.Schema != nil {
|
||||
return yaml.Marshal(s.Schema)
|
||||
}
|
||||
|
||||
if s.Schema == nil && !s.Allows {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s JSONSchemaPropsOrStringArray) MarshalJSON() ([]byte, error) {
|
||||
if len(s.Property) > 0 {
|
||||
return json.Marshal(s.Property)
|
||||
}
|
||||
if s.Schema != nil {
|
||||
return json.Marshal(s.Schema)
|
||||
}
|
||||
return []byte("null"), nil
|
||||
}
|
||||
|
||||
func (s *JSONSchemaPropsOrStringArray) UnmarshalJSON(data []byte) error {
|
||||
var first byte
|
||||
if len(data) > 1 {
|
||||
first = data[0]
|
||||
}
|
||||
var nw JSONSchemaPropsOrStringArray
|
||||
if first == '{' {
|
||||
var sch JSONSchemaProps
|
||||
if err := json.Unmarshal(data, &sch); err != nil {
|
||||
return err
|
||||
}
|
||||
nw.Schema = &sch
|
||||
}
|
||||
if first == '[' {
|
||||
if err := json.Unmarshal(data, &nw.Property); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
*s = nw
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s JSONSchemaPropsOrStringArray) MarshalYAML() (interface{}, error) {
|
||||
if len(s.Property) > 0 {
|
||||
return s.Property, nil
|
||||
}
|
||||
if s.Schema != nil {
|
||||
return s.Schema, nil
|
||||
}
|
||||
return "null", nil
|
||||
}
|
||||
393
pkg/cmd/hgctl/plugin/types/meta.go
Normal file
393
pkg/cmd/hgctl/plugin/types/meta.go
Normal file
@@ -0,0 +1,393 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/iancoleman/orderedmap"
|
||||
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
|
||||
)
|
||||
|
||||
// WasmPluginMeta is used to describe WASM plugin metadata,
|
||||
// see https://higress.io/en-us/docs/user/wasm-image-spec/
|
||||
type WasmPluginMeta struct {
|
||||
APIVersion string `json:"apiVersion" yaml:"apiVersion"`
|
||||
Info WasmPluginInfo `json:"info" yaml:"info"`
|
||||
Spec WasmPluginSpec `json:"spec" yaml:"spec"`
|
||||
}
|
||||
|
||||
func defaultWsamPluginMeta() *WasmPluginMeta {
|
||||
return &WasmPluginMeta{
|
||||
APIVersion: "1.0.0",
|
||||
Info: WasmPluginInfo{
|
||||
Category: CategoryCustom,
|
||||
Name: "Unnamed",
|
||||
XTitleI18n: make(map[I18nType]string),
|
||||
XDescriptionI18n: make(map[I18nType]string),
|
||||
Version: "0.1.0",
|
||||
},
|
||||
Spec: WasmPluginSpec{
|
||||
Phase: PhaseUnspecified,
|
||||
Priority: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ParseSpecYAML parses the `spec.yaml` to WasmPluginMeta
|
||||
func ParseSpecYAML(spec string) (*WasmPluginMeta, error) {
|
||||
f, err := os.Open(spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var m WasmPluginMeta
|
||||
dc := k8syaml.NewYAMLOrJSONDecoder(f, 4096)
|
||||
if err = dc.Decode(&m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
// ParseGoSrc parses the config model of the golang WASM plugin project to WasmPluginMeta
|
||||
func ParseGoSrc(dir, model string) (*WasmPluginMeta, error) {
|
||||
mp, err := NewModelParser(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m, err := mp.GetModel(model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
meta := defaultWsamPluginMeta()
|
||||
meta.setByConfigModel(m)
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
func (meta *WasmPluginMeta) setByConfigModel(model *Model) {
|
||||
_, schema := recursiveSetSchema(model, nil)
|
||||
meta.Spec.ConfigSchema.OpenAPIV3Schema = schema
|
||||
meta.setModelAnnotations(model.Doc)
|
||||
}
|
||||
|
||||
func recursiveSetSchema(model *Model, parent *JSONSchemaProps) (string, *JSONSchemaProps) {
|
||||
cur := NewJSONSchemaProps()
|
||||
cur.Type = model.Type
|
||||
if parent != nil {
|
||||
cur.HandleFieldAnnotations(model.Doc)
|
||||
}
|
||||
newName := cur.HandleFieldTags(model.Tag, parent, model.Name)
|
||||
if IsArray(model.Type) {
|
||||
item := NewJSONSchemaProps()
|
||||
item.Type = GetItemType(cur.Type)
|
||||
cur.Type = "array"
|
||||
if IsObject(item.Type) {
|
||||
item.Properties = make(map[string]JSONSchemaProps)
|
||||
for _, field := range model.Fields {
|
||||
name, child := recursiveSetSchema(&field, cur)
|
||||
item.Properties[name] = *child
|
||||
}
|
||||
}
|
||||
cur.Items = &JSONSchemaPropsOrArray{Schema: item}
|
||||
} else if IsObject(model.Type) { // type may be `array of object`, and it is handled in the first branch
|
||||
for _, field := range model.Fields {
|
||||
name, child := recursiveSetSchema(&field, cur)
|
||||
cur.Properties[name] = *child
|
||||
}
|
||||
}
|
||||
return newName, cur
|
||||
}
|
||||
|
||||
func (meta *WasmPluginMeta) setModelAnnotations(comment string) {
|
||||
as := GetAnnotations(comment)
|
||||
for _, a := range as {
|
||||
switch a.Type {
|
||||
// Info
|
||||
case ACategory:
|
||||
meta.Info.Category = Category(a.Text)
|
||||
case AName:
|
||||
meta.Info.Name = a.Text
|
||||
case ATitle:
|
||||
if meta.Info.Title == "" {
|
||||
meta.Info.Title = a.Text
|
||||
}
|
||||
meta.Info.XTitleI18n[a.I18nType] = a.Text
|
||||
case ADescription:
|
||||
if meta.Info.Description == "" {
|
||||
meta.Info.Description = a.Text
|
||||
}
|
||||
meta.Info.XDescriptionI18n[a.I18nType] = a.Text
|
||||
case AIconUrl:
|
||||
meta.Info.IconUrl = a.Text
|
||||
case AVersion:
|
||||
meta.Info.Version = a.Text
|
||||
case AContactName:
|
||||
meta.Info.Contact.Name = a.Text
|
||||
case AContactUrl:
|
||||
meta.Info.Contact.Url = a.Text
|
||||
case AContactEmail:
|
||||
meta.Info.Contact.Email = a.Text
|
||||
|
||||
// Spec
|
||||
case APhase:
|
||||
meta.Spec.Phase = Phase(a.Text)
|
||||
case APriority:
|
||||
priority, err := strconv.ParseInt(a.Text, 10, 64)
|
||||
if err != nil {
|
||||
priority = 0
|
||||
}
|
||||
meta.Spec.Priority = priority
|
||||
|
||||
// Schema
|
||||
case AExample:
|
||||
meta.Spec.ConfigSchema.OpenAPIV3Schema.Example = &JSON{Raw: []byte(a.Text)}
|
||||
case AScope:
|
||||
meta.Spec.ConfigSchema.OpenAPIV3Schema.Scope = Scope(a.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type WasmPluginInfo struct {
|
||||
Category Category `json:"category" yaml:"category"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Title string `json:"title,omitempty" yaml:"title,omitempty"`
|
||||
XTitleI18n map[I18nType]string `json:"x-title-i18n,omitempty" yaml:"x-title-i18n,omitempty"`
|
||||
Description string `json:"description,omitempty" yaml:"description,omitempty"`
|
||||
XDescriptionI18n map[I18nType]string `json:"x-description-i18n,omitempty" yaml:"x-description-i18n,omitempty"`
|
||||
IconUrl string `json:"iconUrl,omitempty" yaml:"iconUrl,omitempty"`
|
||||
Version string `json:"version" yaml:"version"`
|
||||
Contact Contact `json:"contact,omitempty" yaml:"contact,omitempty"`
|
||||
}
|
||||
|
||||
type Category string
|
||||
|
||||
const (
|
||||
CategoryAuth Category = "auth"
|
||||
CategorySecurity Category = "security"
|
||||
CategoryProtocol Category = "protocol"
|
||||
CategoryFlowControl Category = "flow-control"
|
||||
CategoryFlowMonitor Category = "flow-monitor"
|
||||
CategoryCustom Category = "custom"
|
||||
CategoryDefault = CategoryCustom
|
||||
)
|
||||
|
||||
const (
|
||||
IconAuth = "https://img.alicdn.com/imgextra/i4/O1CN01BPFGlT1pGZ2VDLgaH_!!6000000005333-2-tps-42-42.png"
|
||||
IconSecurity = "https://img.alicdn.com/imgextra/i1/O1CN01jKT9vC1O059vNaq5u_!!6000000001642-2-tps-42-42.png"
|
||||
IconProtocol = "https://img.alicdn.com/imgextra/i2/O1CN01xIywow1mVGuRUjbhe_!!6000000004959-2-tps-42-42.png"
|
||||
IconFlowControl = "https://img.alicdn.com/imgextra/i3/O1CN01bAFa9k1t1gdQcVTH0_!!6000000005842-2-tps-42-42.png"
|
||||
IconFlowMonitor = "https://img.alicdn.com/imgextra/i4/O1CN01aet3s61MoLOEEhRIo_!!6000000001481-2-tps-42-42.png"
|
||||
IconCustom = "https://img.alicdn.com/imgextra/i1/O1CN018iKKih1iVx287RltL_!!6000000004419-2-tps-42-42.png"
|
||||
IconDefault = IconCustom
|
||||
)
|
||||
|
||||
func Category2IconUrl(category Category) string {
|
||||
switch category {
|
||||
case CategoryAuth:
|
||||
return IconAuth
|
||||
case CategorySecurity:
|
||||
return IconSecurity
|
||||
case CategoryProtocol:
|
||||
return IconProtocol
|
||||
case CategoryFlowControl:
|
||||
return IconFlowControl
|
||||
case CategoryFlowMonitor:
|
||||
return IconFlowMonitor
|
||||
case CategoryCustom:
|
||||
return IconCustom
|
||||
default:
|
||||
return IconDefault
|
||||
}
|
||||
}
|
||||
|
||||
type I18nType string
|
||||
|
||||
const (
|
||||
I18nZH_CN I18nType = "zh-CN" // default
|
||||
I18nEN_US I18nType = "en-US"
|
||||
I18nUndefined I18nType = "undefined" // i18n type is empty in the annotation
|
||||
I18nUnknown I18nType = "unknown"
|
||||
I18nDefault = I18nEN_US
|
||||
)
|
||||
|
||||
func str2I18nType(typ string) I18nType {
|
||||
switch strings.ToLower(typ) {
|
||||
case "zh-cn":
|
||||
return I18nZH_CN
|
||||
case "en-us":
|
||||
return I18nEN_US
|
||||
default:
|
||||
return I18nUnknown
|
||||
}
|
||||
}
|
||||
|
||||
type Contact struct {
|
||||
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
||||
Url string `json:"url,omitempty" yaml:"url,omitempty"`
|
||||
Email string `json:"email,omitempty" yaml:"email,omitempty"`
|
||||
}
|
||||
|
||||
type WasmPluginSpec struct {
|
||||
// Phase refers to https://istio.io/latest/docs/reference/config/proxy_extensions/wasm-plugin/#PluginPhase
|
||||
Phase Phase `json:"phase" yaml:"phase"`
|
||||
|
||||
// Priority refers to https://istio.io/latest/docs/reference/config/proxy_extensions/wasm-plugin/#WasmPlugin
|
||||
Priority int64 `json:"priority" yaml:"priority"`
|
||||
|
||||
ConfigSchema ConfigSchema `json:"configSchema" yaml:"configSchema"`
|
||||
}
|
||||
|
||||
type Phase string
|
||||
|
||||
const (
|
||||
PhaseUnspecified Phase = "UNSPECIFIED_PHASE"
|
||||
PhaseAuthn Phase = "AUTHN"
|
||||
PhaseAuthz Phase = "AUTHZ"
|
||||
PhaseStats Phase = "STATS"
|
||||
PhaseDefault = PhaseUnspecified
|
||||
)
|
||||
|
||||
type ConfigSchema struct {
|
||||
OpenAPIV3Schema *JSONSchemaProps `json:"openAPIV3Schema" yaml:"openAPIV3Schema"`
|
||||
}
|
||||
|
||||
// GetConfigExample returns a pretty WASM plugin config example
|
||||
func (meta *WasmPluginMeta) GetConfigExample() string {
|
||||
s := meta.Spec.ConfigSchema.OpenAPIV3Schema
|
||||
if s != nil {
|
||||
return s.GetExample()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getLanguageUnionOrderMap returns a ordered map of language union of title and description.
|
||||
// If there is a language type in title that description does not have, the value is "No description"
|
||||
func (meta *WasmPluginMeta) getLanguageUnionOrderMap() *orderedmap.OrderedMap {
|
||||
m := orderedmap.New()
|
||||
for i18n, desc := range meta.Info.XDescriptionI18n {
|
||||
m.Set(string(i18n), desc)
|
||||
}
|
||||
for i18n := range meta.Info.XTitleI18n {
|
||||
if _, ok := m.Get(string(i18n)); !ok {
|
||||
m.Set(string(i18n), "No description")
|
||||
}
|
||||
}
|
||||
if len(m.Keys()) == 0 {
|
||||
m.Set(string(I18nEN_US), "No description")
|
||||
}
|
||||
m.SortKeys(sort.Strings)
|
||||
return m
|
||||
}
|
||||
|
||||
// WasmUsage is used to describe WASM plugin usage in the Markdown document
|
||||
type WasmUsage struct {
|
||||
I18nType I18nType
|
||||
Description string
|
||||
ConfigEntries []ConfigEntry
|
||||
Example string
|
||||
}
|
||||
|
||||
type ConfigEntry struct {
|
||||
Name string
|
||||
Type string
|
||||
Requirement string
|
||||
Default string
|
||||
Description string
|
||||
}
|
||||
|
||||
// GetUsages returns WASM plugin usages in different languages
|
||||
func (meta *WasmPluginMeta) GetUsages() ([]WasmUsage, error) {
|
||||
usages := make([]WasmUsage, 0)
|
||||
example := meta.GetConfigExample()
|
||||
m := meta.getLanguageUnionOrderMap()
|
||||
for _, i18n := range m.Keys() {
|
||||
desc, ok := m.Get(i18n)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
u := WasmUsage{
|
||||
I18nType: I18nType(i18n),
|
||||
Description: desc.(string),
|
||||
ConfigEntries: make([]ConfigEntry, 0),
|
||||
Example: example,
|
||||
}
|
||||
getConfigEntries(meta.Spec.ConfigSchema.OpenAPIV3Schema, &u.ConfigEntries, I18nType(i18n))
|
||||
usages = append(usages, u)
|
||||
}
|
||||
|
||||
return usages, nil
|
||||
}
|
||||
|
||||
func getConfigEntries(schema *JSONSchemaProps, entries *[]ConfigEntry, i18n I18nType) {
|
||||
doGetConfigEntries(schema, entries, "", "", i18n, false)
|
||||
}
|
||||
|
||||
func doGetConfigEntries(schema *JSONSchemaProps, entries *[]ConfigEntry, parentName, name string, i18n I18nType, required bool) {
|
||||
newName := constructName(parentName, name)
|
||||
switch schema.Type {
|
||||
case "object":
|
||||
m := schema.GetPropertiesOrderMap()
|
||||
for _, fieldName := range m.Keys() {
|
||||
val, ok := m.Get(fieldName)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
props := val.(JSONSchemaProps)
|
||||
required = schema.IsRequired(fieldName)
|
||||
doGetConfigEntries(&props, entries, newName, fieldName, i18n, required)
|
||||
}
|
||||
case "array":
|
||||
itemType := schema.Items.Schema.Type
|
||||
e := ConfigEntry{
|
||||
Name: newName,
|
||||
Type: ArrayPrefix + itemType,
|
||||
Requirement: schema.JoinRequirementsBy(i18n, required),
|
||||
Default: schema.GetDefaultValue(),
|
||||
Description: schema.XDescriptionI18n[i18n],
|
||||
}
|
||||
*entries = append(*entries, e)
|
||||
if itemType == "object" {
|
||||
doGetConfigEntries(schema.Items.Schema, entries, newName+"[*]", "", i18n, false)
|
||||
}
|
||||
default:
|
||||
e := ConfigEntry{
|
||||
Name: newName,
|
||||
Type: schema.Type,
|
||||
Requirement: schema.JoinRequirementsBy(i18n, required),
|
||||
Default: schema.GetDefaultValue(),
|
||||
Description: schema.XDescriptionI18n[i18n],
|
||||
}
|
||||
*entries = append(*entries, e)
|
||||
}
|
||||
}
|
||||
|
||||
func constructName(parent, name string) string {
|
||||
newName := name
|
||||
if parent != "" {
|
||||
if name != "" {
|
||||
newName = fmt.Sprintf("%s.%s", parent, name)
|
||||
} else {
|
||||
newName = parent
|
||||
}
|
||||
}
|
||||
return newName
|
||||
}
|
||||
391
pkg/cmd/hgctl/plugin/types/model_parser.go
Normal file
391
pkg/cmd/hgctl/plugin/types/model_parser.go
Normal file
@@ -0,0 +1,391 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/structtag"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
ArrayPrefix = "array of "
|
||||
ObjectSuffix = "object"
|
||||
)
|
||||
|
||||
// IsArray returns true if the given type is an `array of <type>`
|
||||
func IsArray(typ string) bool {
|
||||
return strings.HasPrefix(typ, ArrayPrefix)
|
||||
}
|
||||
|
||||
// GetItemType returns the item type of array, e.g.: array of int -> int
|
||||
func GetItemType(typ string) string {
|
||||
return strings.TrimPrefix(typ, ArrayPrefix)
|
||||
}
|
||||
|
||||
// IsObject returns true if the given type is an `object` or an `array of object`
|
||||
func IsObject(typ string) bool {
|
||||
return strings.HasSuffix(typ, ObjectSuffix)
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInvalidModel = errors.New("invalid model")
|
||||
ErrInvalidFiledType = errors.New("invalid field type")
|
||||
)
|
||||
|
||||
type ModelParser struct {
|
||||
structs map[string]*astNode
|
||||
|
||||
// alias for a basic type, such as type MyInt int: MyInt -> int
|
||||
// TODO(WeixinX): Support alias for package name
|
||||
alias map[string]*astNode
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
Name string
|
||||
Type string
|
||||
Doc string
|
||||
Tag string
|
||||
Fields []Model
|
||||
}
|
||||
|
||||
type astNode struct {
|
||||
name string
|
||||
doc string
|
||||
expr ast.Expr
|
||||
}
|
||||
|
||||
func (m *Model) Inspect(f func(model *Model) bool) {
|
||||
ctn := f(m)
|
||||
if !ctn {
|
||||
return
|
||||
}
|
||||
|
||||
for _, field := range m.Fields {
|
||||
field.Inspect(f)
|
||||
}
|
||||
}
|
||||
|
||||
// NewModelParser new a model parser based on the dir where the given model exists
|
||||
func NewModelParser(dir string) (*ModelParser, error) {
|
||||
pkgs, err := walkGoSrc(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p := &ModelParser{
|
||||
structs: make(map[string]*astNode),
|
||||
alias: make(map[string]*astNode),
|
||||
}
|
||||
for _, pkg := range pkgs {
|
||||
for _, f := range pkg.Files {
|
||||
for _, decl := range f.Decls {
|
||||
x, ok := decl.(*ast.GenDecl)
|
||||
if !ok || x.Tok != token.TYPE {
|
||||
continue
|
||||
}
|
||||
for _, spec := range x.Specs {
|
||||
ts, ok := spec.(*ast.TypeSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
switch t := ts.Type.(type) {
|
||||
case *ast.StructType:
|
||||
if !t.Struct.IsValid() {
|
||||
continue
|
||||
}
|
||||
s := &astNode{
|
||||
name: ts.Name.String(),
|
||||
expr: t,
|
||||
}
|
||||
if pkg.Name != "main" { // ignore main package prefix
|
||||
s.name = fmt.Sprintf("%s.%s", pkg.Name, s.name)
|
||||
}
|
||||
if x.Doc != nil {
|
||||
s.doc = x.Doc.Text()
|
||||
}
|
||||
p.structs[s.name] = s
|
||||
case *ast.InterfaceType:
|
||||
continue
|
||||
default: // for alias, such as `type MyInt int`
|
||||
alias := ts.Name.String()
|
||||
if pkg.Name != "main" {
|
||||
alias = fmt.Sprintf("%s.%s", pkg.Name, alias)
|
||||
}
|
||||
name, err := p.getModelName(t)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
p.alias[alias] = &astNode{
|
||||
name: name,
|
||||
expr: t,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// gets the true type (ast node) of the alias
|
||||
for alias := range p.alias {
|
||||
n := p.recursiveAlias(alias)
|
||||
if n != nil {
|
||||
p.alias[alias] = n
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func walkGoSrc(dir string) (map[string]*ast.Package, error) {
|
||||
info, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return nil, errors.Errorf("%q is not a directory", dir)
|
||||
}
|
||||
|
||||
fset := token.NewFileSet()
|
||||
pkgs := make(map[string]*ast.Package)
|
||||
walk := func(path string, info fs.FileInfo, err error) error {
|
||||
if !info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
tmp, err := parser.ParseDir(fset, path, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for k, v := range tmp {
|
||||
pkgs[k] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := filepath.Walk(dir, walk); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to walk path %q", dir)
|
||||
}
|
||||
return pkgs, nil
|
||||
}
|
||||
|
||||
func (p *ModelParser) recursiveAlias(alias string) *astNode {
|
||||
if s, ok := p.structs[alias]; ok {
|
||||
return s
|
||||
}
|
||||
if n, ok := p.alias[alias]; ok {
|
||||
if n.name != alias {
|
||||
ret := p.recursiveAlias(n.name)
|
||||
if ret != nil {
|
||||
return ret
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetModel return the specified model
|
||||
func (p *ModelParser) GetModel(model string) (*Model, error) {
|
||||
fields, err := p.parseModelFields(model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := &Model{
|
||||
Name: model,
|
||||
Type: "object",
|
||||
Fields: fields,
|
||||
}
|
||||
m.setDoc(p.structs[model].doc)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (p *ModelParser) parseModelFields(model string) (fields []Model, err error) {
|
||||
var s *astNode
|
||||
if _, ok := p.structs[model]; ok {
|
||||
s = p.structs[model]
|
||||
} else if _, ok = p.alias[model]; ok {
|
||||
s = p.alias[model]
|
||||
} else {
|
||||
return nil, ErrInvalidModel
|
||||
}
|
||||
|
||||
st, ok := s.expr.(*ast.StructType)
|
||||
if !ok || st.Fields == nil {
|
||||
return nil, ErrInvalidModel
|
||||
}
|
||||
pkgName := ""
|
||||
if idx := strings.Index(model, "."); idx != -1 {
|
||||
pkgName = model[:idx+1] // pkgName includes "."
|
||||
}
|
||||
for _, field := range st.Fields.List {
|
||||
if skipField(field) {
|
||||
continue
|
||||
}
|
||||
fd := Model{Name: field.Names[0].String()}
|
||||
if field.Doc != nil {
|
||||
fd.setDoc(field.Doc.Text())
|
||||
}
|
||||
if field.Tag != nil {
|
||||
ignore, err := fd.setTag(field.Tag.Value)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to parse tag %q of the field %q", fd.Tag, fd.Name)
|
||||
}
|
||||
if ignore {
|
||||
continue
|
||||
}
|
||||
}
|
||||
fd.Type, err = p.parseFiledType(pkgName, field.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if IsObject(fd.Type) {
|
||||
subModel, err := p.getModelName(field.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fd.Fields, err = p.parseModelFields(subModel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
fields = append(fields, fd)
|
||||
}
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
func skipField(field *ast.Field) bool {
|
||||
name := field.Names
|
||||
return field == nil || name == nil || len(name) < 1 || name[0] == nil || name[0].String() == "_"
|
||||
}
|
||||
|
||||
func (m *Model) setDoc(str string) {
|
||||
m.Doc = strings.TrimSpace(str)
|
||||
}
|
||||
|
||||
func (m *Model) setTag(str string) (bool, error) {
|
||||
str = strings.Trim(str, "` ")
|
||||
if str == "" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
ignore := false
|
||||
tag, err := structtag.Parse(str)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
m.Tag = str
|
||||
val, err := tag.Get("yaml")
|
||||
if err == nil {
|
||||
if val.Name == "-" || val.Name == "" {
|
||||
ignore = true
|
||||
}
|
||||
}
|
||||
return ignore, nil
|
||||
}
|
||||
|
||||
func (p *ModelParser) getModelName(typ ast.Expr) (string, error) {
|
||||
return p.doGetModelName("", typ)
|
||||
}
|
||||
|
||||
func (p *ModelParser) doGetModelName(pkgName string, typ ast.Expr) (string, error) {
|
||||
switch t := typ.(type) {
|
||||
case *ast.StarExpr: // *int -> int
|
||||
return p.doGetModelName(pkgName, t.X)
|
||||
case *ast.ArrayType: // slice or array
|
||||
return p.doGetModelName(pkgName, t.Elt)
|
||||
case *ast.SelectorExpr: // <pkg_name>.<field_name>
|
||||
pkg, ok := t.X.(*ast.Ident)
|
||||
if !ok {
|
||||
return "", ErrInvalidFiledType
|
||||
}
|
||||
pName := pkg.Name + "."
|
||||
return p.doGetModelName(pName, t.Sel)
|
||||
case *ast.Ident:
|
||||
return pkgName + t.Name, nil
|
||||
default:
|
||||
return "", ErrInvalidFiledType
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ModelParser) parseFiledType(pkgName string, typ ast.Expr) (string, error) {
|
||||
switch t := typ.(type) {
|
||||
case *ast.StructType: // nested struct
|
||||
return string(JsonTypeObject), nil
|
||||
case *ast.StarExpr: // *int -> int
|
||||
return p.parseFiledType(pkgName, t.X)
|
||||
case *ast.ArrayType: // slice or array
|
||||
ret, err := p.parseFiledType(pkgName, t.Elt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ArrayPrefix + ret, nil
|
||||
case *ast.SelectorExpr: // <pkg_name>.<field_name>
|
||||
pkg, ok := t.X.(*ast.Ident)
|
||||
if !ok {
|
||||
return "", ErrInvalidFiledType
|
||||
}
|
||||
pName := pkg.Name + "."
|
||||
return p.parseFiledType(pName, t.Sel)
|
||||
case *ast.Ident:
|
||||
fName := pkgName + t.Name
|
||||
if _, ok := p.structs[fName]; ok {
|
||||
return string(JsonTypeObject), nil
|
||||
}
|
||||
if alias, ok := p.alias[fName]; ok {
|
||||
return p.parseFiledType(pkgName, alias.expr)
|
||||
}
|
||||
jsonType, err := convert2JsonType(t.Name)
|
||||
return string(jsonType), err
|
||||
default:
|
||||
return "", ErrInvalidFiledType
|
||||
}
|
||||
}
|
||||
|
||||
func convert2JsonType(typ string) (JsonType, error) {
|
||||
switch typ {
|
||||
case "int", "int8", "int16", "int32", "int64",
|
||||
"uint", "uint8", "uint16", "uint32", "uint64":
|
||||
return JsonTypeInteger, nil
|
||||
case "float32", "float64":
|
||||
return JsonTypeNumber, nil
|
||||
case "bool":
|
||||
return JsonTypeBoolean, nil
|
||||
case "string":
|
||||
return JsonTypeString, nil
|
||||
case "struct":
|
||||
return JsonTypeObject, nil
|
||||
default:
|
||||
return "", ErrInvalidFiledType
|
||||
}
|
||||
}
|
||||
|
||||
type JsonType string
|
||||
|
||||
const (
|
||||
JsonTypeInteger JsonType = "integer"
|
||||
JsonTypeNumber JsonType = "number"
|
||||
JsonTypeBoolean JsonType = "boolean"
|
||||
JsonTypeString JsonType = "string"
|
||||
JsonTypeObject JsonType = "object"
|
||||
JsonTypeArray JsonType = "array"
|
||||
)
|
||||
379
pkg/cmd/hgctl/plugin/types/model_parser_test.go
Normal file
379
pkg/cmd/hgctl/plugin/types/model_parser_test.go
Normal file
@@ -0,0 +1,379 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetModel(t *testing.T) {
|
||||
var (
|
||||
BasicStructField = []Model{
|
||||
{
|
||||
Name: "Name",
|
||||
Type: "string",
|
||||
},
|
||||
{
|
||||
Name: "Age",
|
||||
Type: "integer",
|
||||
},
|
||||
{
|
||||
Name: "Married",
|
||||
Type: "boolean",
|
||||
},
|
||||
{
|
||||
Name: "Salary",
|
||||
Type: "number",
|
||||
},
|
||||
}
|
||||
|
||||
ExternalStructField = []Model{
|
||||
{
|
||||
Name: "one",
|
||||
Type: "string",
|
||||
},
|
||||
{
|
||||
Name: "two",
|
||||
Type: "integer",
|
||||
},
|
||||
{
|
||||
Name: "three",
|
||||
Type: "array of boolean",
|
||||
},
|
||||
}
|
||||
|
||||
NestedStructField = []Model{
|
||||
{
|
||||
Name: "Simple",
|
||||
Type: "string",
|
||||
},
|
||||
{
|
||||
Name: "Complex",
|
||||
Type: "array of integer",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
expected *Model
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "TestBasicStruct",
|
||||
expected: &Model{
|
||||
Name: "TestBasicStruct",
|
||||
Type: "object",
|
||||
Fields: BasicStructField,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TestComplexStruct",
|
||||
expected: &Model{
|
||||
Name: "TestComplexStruct",
|
||||
Type: "object",
|
||||
Fields: []Model{
|
||||
{
|
||||
Name: "Array",
|
||||
Type: "array of integer",
|
||||
},
|
||||
{
|
||||
Name: "Slice",
|
||||
Type: "array of string",
|
||||
},
|
||||
{
|
||||
Name: "Pointer",
|
||||
Type: "string",
|
||||
},
|
||||
{
|
||||
Name: "PPPointer",
|
||||
Type: "boolean",
|
||||
},
|
||||
{
|
||||
Name: "ArrayPointer",
|
||||
Type: "array of integer",
|
||||
},
|
||||
{
|
||||
Name: "SlicePointer",
|
||||
Type: "array of integer",
|
||||
},
|
||||
{
|
||||
Name: "StructPointerSlice",
|
||||
Type: "array of object",
|
||||
Fields: BasicStructField,
|
||||
},
|
||||
{
|
||||
Name: "StructArrayPointer",
|
||||
Type: "array of object",
|
||||
Fields: BasicStructField,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TestAliasStruct",
|
||||
expected: &Model{
|
||||
Name: "TestAliasStruct",
|
||||
Type: "object",
|
||||
Fields: []Model{
|
||||
{
|
||||
Name: "MyString",
|
||||
Type: "string",
|
||||
},
|
||||
{
|
||||
Name: "MyPointerInt",
|
||||
Type: "integer",
|
||||
},
|
||||
{
|
||||
Name: "MyStruct",
|
||||
Type: "object",
|
||||
Fields: BasicStructField,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TestExternalStruct",
|
||||
expected: &Model{
|
||||
Name: "TestExternalStruct",
|
||||
Type: "object",
|
||||
Fields: []Model{
|
||||
{
|
||||
Name: "InternalFloat",
|
||||
Type: "number",
|
||||
},
|
||||
{
|
||||
Name: "ExStruct",
|
||||
Type: "object",
|
||||
Fields: ExternalStructField,
|
||||
},
|
||||
{
|
||||
Name: "ExternalInt",
|
||||
Type: "integer",
|
||||
},
|
||||
{
|
||||
Name: "ExBool",
|
||||
Type: "boolean",
|
||||
},
|
||||
{
|
||||
Name: "ExSlice",
|
||||
Type: "array of string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TestNestedStruct",
|
||||
expected: &Model{
|
||||
Name: "TestNestedStruct",
|
||||
Type: "object",
|
||||
Fields: []Model{
|
||||
{
|
||||
Name: "NestedStruct",
|
||||
Type: "object",
|
||||
Fields: []Model{
|
||||
{
|
||||
Name: "NestedStruct",
|
||||
Type: "object",
|
||||
Fields: NestedStructField,
|
||||
},
|
||||
{
|
||||
Name: "NestedInt",
|
||||
Type: "integer",
|
||||
},
|
||||
{
|
||||
Name: "NestedString",
|
||||
Type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ext.TestExStruct",
|
||||
expected: &Model{
|
||||
Name: "ext.TestExStruct",
|
||||
Type: "object",
|
||||
Fields: ExternalStructField,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ext.TestNestedStruct",
|
||||
expected: &Model{
|
||||
Name: "ext.TestNestedStruct",
|
||||
Type: "object",
|
||||
Fields: []Model{
|
||||
{
|
||||
Name: "NestedStruct",
|
||||
Type: "object",
|
||||
Fields: NestedStructField,
|
||||
},
|
||||
{
|
||||
Name: "NestedInt",
|
||||
Type: "integer",
|
||||
},
|
||||
{
|
||||
Name: "NestedString",
|
||||
Type: "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
p, err := NewModelParser("./testdata/types")
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
actual, err := p.GetModel(c.name)
|
||||
if c.errMsg != "" {
|
||||
require.EqualError(t, err, c.errMsg)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, c.expected, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseStructAndAlias(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
dir string
|
||||
expectedStructs map[string]struct{}
|
||||
expectedAlias map[string]string
|
||||
}{
|
||||
{
|
||||
name: "Basic",
|
||||
dir: "./testdata/types",
|
||||
expectedStructs: map[string]struct{}{
|
||||
"TestBasicStruct": {},
|
||||
"TestComplexStruct": {},
|
||||
"TestAliasStruct": {},
|
||||
"TestExternalStruct": {},
|
||||
"TestNestedStruct": {},
|
||||
"ext.TestExStruct": {},
|
||||
"ext.TestNestedStruct": {},
|
||||
"nested.TestNestedStruct": {},
|
||||
},
|
||||
expectedAlias: map[string]string{
|
||||
"MyString": "string",
|
||||
"MyPointerInt": "int",
|
||||
"MyStruct": "TestBasicStruct",
|
||||
"NestedAlias": "nested.TestNestedStruct",
|
||||
"NestedBasicAlias": "bool",
|
||||
"ext.ExAlias": "nested.TestNestedStruct",
|
||||
"ext.ExPointerInt": "int",
|
||||
"ext.ExBool": "bool",
|
||||
"ext.ExSlice": "string",
|
||||
"nested.NestedInt": "int",
|
||||
"nested.NestedString": "string",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
p, err := NewModelParser(c.dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
actualStructs := make(map[string]struct{})
|
||||
for _, s := range p.structs {
|
||||
actualStructs[s.name] = struct{}{}
|
||||
}
|
||||
require.Equal(t, c.expectedStructs, actualStructs)
|
||||
|
||||
actualAlias := make(map[string]string)
|
||||
for name, alias := range p.alias {
|
||||
actualAlias[name] = alias.name
|
||||
}
|
||||
require.Equal(t, c.expectedAlias, actualAlias)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStructFieldDocAndTag(t *testing.T) {
|
||||
var BasicStructField = []Model{
|
||||
{
|
||||
Name: "Name",
|
||||
Type: "string",
|
||||
Doc: "Name, specify username",
|
||||
Tag: `yaml:"name" required:"true" minLength:"1" maxLength:"32"`,
|
||||
},
|
||||
{
|
||||
Name: "Age",
|
||||
Type: "integer",
|
||||
Doc: "Age, specify age",
|
||||
Tag: `yaml:"age" required:"true" minimum:"0" maximum:"140"`,
|
||||
},
|
||||
{
|
||||
Name: "Married",
|
||||
Type: "boolean",
|
||||
Doc: "Married, specify marital status [true, false]\nand optional",
|
||||
Tag: `yaml:"married" required:"false"`,
|
||||
},
|
||||
{
|
||||
Name: "Salary",
|
||||
Type: "number",
|
||||
Doc: "Salary, specify income status, optional",
|
||||
Tag: `yaml:"salary" required:"false"`,
|
||||
},
|
||||
{
|
||||
Name: "Children",
|
||||
Type: "array of string",
|
||||
Doc: "Children, specify a list of children's names, optional",
|
||||
Tag: `yaml:"children" required:"false"`,
|
||||
},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
model string
|
||||
expected []Model
|
||||
}{
|
||||
{
|
||||
name: "TestBasicDocTag",
|
||||
model: "TestBasicDocTag",
|
||||
expected: BasicStructField,
|
||||
},
|
||||
{
|
||||
name: "TestNestedStructDocTag",
|
||||
model: "TestNestedStructDocTag",
|
||||
expected: []Model{
|
||||
{
|
||||
Name: "Struct",
|
||||
Type: "array of object",
|
||||
Doc: "This is the comment of the nested struct field",
|
||||
Tag: `yaml:"struct" required:"true" minItems:"1" maxItems:"10"`,
|
||||
Fields: BasicStructField,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
p, err := NewModelParser("./testdata/doc_tag")
|
||||
require.NoError(t, err)
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
m, err := p.GetModel(c.model)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, c.expected, m.Fields)
|
||||
})
|
||||
}
|
||||
}
|
||||
426
pkg/cmd/hgctl/plugin/types/schema.go
Normal file
426
pkg/cmd/hgctl/plugin/types/schema.go
Normal file
@@ -0,0 +1,426 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin/utils"
|
||||
|
||||
"github.com/fatih/structtag"
|
||||
"github.com/iancoleman/orderedmap"
|
||||
)
|
||||
|
||||
// JSONSchemaProps is a JSON-Schema following Specification Draft 4 (http://json-schema.org/).
|
||||
// Borrowed from https://github.com/kubernetes/apiextensions-apiserver/blob/master/pkg/apis/apiextensions/v1/types_jsonschema.go
|
||||
type JSONSchemaProps struct {
|
||||
ID string `json:"id,omitempty" yaml:"id,omitempty"`
|
||||
Schema JSONSchemaURL `json:"$schema,omitempty" yaml:"$schema,omitempty"`
|
||||
Ref *string `json:"$ref,omitempty" yaml:"$ref,omitempty"`
|
||||
Type string `json:"type,omitempty" yaml:"type,omitempty"`
|
||||
Format string `json:"format,omitempty" yaml:"format,omitempty"`
|
||||
Scope Scope `json:"scope,omitempty" yaml:"scope,omitempty"`
|
||||
Title string `json:"title,omitempty" yaml:"title,omitempty"`
|
||||
XTitleI18n map[I18nType]string `json:"x-title-i18n,omitempty" yaml:"x-title-i18n,omitempty"`
|
||||
Description string `json:"description,omitempty" yaml:"description,omitempty"`
|
||||
XDescriptionI18n map[I18nType]string `json:"x-description-i18n,omitempty" yaml:"x-description-i18n,omitempty"`
|
||||
Default *JSON `json:"default,omitempty" yaml:"default,omitempty"`
|
||||
Minimum *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"`
|
||||
ExclusiveMinimum bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"`
|
||||
Maximum *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"`
|
||||
ExclusiveMaximum bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"`
|
||||
MinLength *int64 `json:"minLength,omitempty" yaml:"minLength,omitempty"`
|
||||
MaxLength *int64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"`
|
||||
Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"`
|
||||
MaxItems *int64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"`
|
||||
MinItems *int64 `json:"minItems,omitempty" yaml:"minItems,omitempty"`
|
||||
UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"`
|
||||
MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"`
|
||||
Enum []JSON `json:"enum,omitempty" yaml:"enum,omitempty"`
|
||||
MinProperties *int64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"`
|
||||
MaxProperties *int64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"`
|
||||
Required []string `json:"required,omitempty" yaml:"required,omitempty"`
|
||||
Items *JSONSchemaPropsOrArray `json:"items,omitempty" yaml:"items,omitempty"`
|
||||
AllOf []JSONSchemaProps `json:"allOf,omitempty" yaml:"allOf,omitempty"`
|
||||
OneOf []JSONSchemaProps `json:"oneOf,omitempty" yaml:"oneOf,omitempty"`
|
||||
AnyOf []JSONSchemaProps `json:"anyOf,omitempty" yaml:"anyOf,omitempty"`
|
||||
Not *JSONSchemaProps `json:"not,omitempty" yaml:"not,omitempty"`
|
||||
Properties map[string]JSONSchemaProps `json:"properties,omitempty" yaml:"properties,omitempty"`
|
||||
AdditionalProperties *JSONSchemaPropsOrBool `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"`
|
||||
PatternProperties map[string]JSONSchemaProps `json:"patternProperties,omitempty" yaml:"patternProperties,omitempty"`
|
||||
Dependencies JSONSchemaDependencies `json:"dependencies,omitempty" yaml:"dependencies,omitempty"`
|
||||
AdditionalItems *JSONSchemaPropsOrBool `json:"additionalItems,omitempty" yaml:"additionalItems,omitempty"`
|
||||
Definitions JSONSchemaDefinitions `json:"definitions,omitempty" yaml:"definitions,omitempty"`
|
||||
ExternalDocs *ExternalDocumentation `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"`
|
||||
Example *JSON `json:"example,omitempty" yaml:"example,omitempty"`
|
||||
Nullable bool `json:"nullable,omitempty" yaml:"nullable,omitempty"`
|
||||
}
|
||||
|
||||
type Scope string
|
||||
|
||||
const (
|
||||
ScopeGlobal Scope = "GLOBAL"
|
||||
ScopeInstance Scope = "INSTANCE"
|
||||
ScopeAll Scope = "ALL"
|
||||
ScopeDefault = ScopeInstance
|
||||
)
|
||||
|
||||
// JSON represents any valid JSON value.
|
||||
// These types are supported: bool, int64, float64, string, []interface{}, map[string]interface{} and nil.
|
||||
type JSON struct {
|
||||
Raw []byte `json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
// JSONSchemaPropsOrArray represents a value that can either be a JSONSchemaProps
|
||||
// or an array of JSONSchemaProps. Mainly here for serialization purposes.
|
||||
type JSONSchemaPropsOrArray struct {
|
||||
Schema *JSONSchemaProps
|
||||
JSONSchemas []JSONSchemaProps
|
||||
}
|
||||
|
||||
// JSONSchemaPropsOrBool represents JSONSchemaProps or a boolean value.
|
||||
// Defaults to true for the boolean property.
|
||||
type JSONSchemaPropsOrBool struct {
|
||||
Allows bool
|
||||
Schema *JSONSchemaProps
|
||||
}
|
||||
|
||||
// JSONSchemaDependencies represent a dependencies property.
|
||||
type JSONSchemaDependencies map[string]JSONSchemaPropsOrStringArray
|
||||
|
||||
// JSONSchemaPropsOrStringArray represents a JSONSchemaProps or a string array.
|
||||
type JSONSchemaPropsOrStringArray struct {
|
||||
Schema *JSONSchemaProps
|
||||
Property []string
|
||||
}
|
||||
|
||||
// JSONSchemaURL represents a schema url.
|
||||
type JSONSchemaURL string
|
||||
|
||||
// JSONSchemaDefinitions contains the models explicitly defined in this spec.
|
||||
type JSONSchemaDefinitions map[string]JSONSchemaProps
|
||||
|
||||
// ExternalDocumentation allows referencing an external resource for extended documentation.
|
||||
type ExternalDocumentation struct {
|
||||
Description string `json:"description,omitempty" yaml:"description,omitempty"`
|
||||
URL string `json:"url,omitempty" yaml:"url,omitempty"`
|
||||
}
|
||||
|
||||
func NewJSONSchemaProps() *JSONSchemaProps {
|
||||
return &JSONSchemaProps{
|
||||
XTitleI18n: make(map[I18nType]string),
|
||||
XDescriptionI18n: make(map[I18nType]string),
|
||||
Properties: make(map[string]JSONSchemaProps),
|
||||
}
|
||||
}
|
||||
|
||||
// IsRequired determines whether the given `name` field is required
|
||||
func (s *JSONSchemaProps) IsRequired(name string) bool {
|
||||
req := false
|
||||
for _, n := range s.Required {
|
||||
if name == n {
|
||||
req = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
// GetDefaultValue returns the default value of the schema
|
||||
func (s *JSONSchemaProps) GetDefaultValue() string {
|
||||
d := "-"
|
||||
if s.Default == nil {
|
||||
return d
|
||||
}
|
||||
if len(s.Default.Raw) > 0 {
|
||||
d = string(s.Default.Raw)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// GetExample returns the pretty example of the schema
|
||||
func (s *JSONSchemaProps) GetExample() string {
|
||||
ret := ""
|
||||
if s.Example != nil && len(s.Example.Raw) > 0 {
|
||||
ret = string(s.Example.Raw)
|
||||
if ret[0] == '{' {
|
||||
// string(s.Example.Raw) might look like (when the schema is generated through go src):
|
||||
// {"allow":["consumer1"],"consumers":[{"credential":"admin:123456","name":"consumer1"}]}
|
||||
var obj interface{}
|
||||
err := json.Unmarshal(s.Example.Raw, &obj)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
b, err := utils.MarshalYamlWithIndent(obj, 2)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
ret = string(b)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// GetPropertiesOrderMap converts the schema Properties map to
|
||||
// an ordered map (dictionary order) and returns it
|
||||
func (s *JSONSchemaProps) GetPropertiesOrderMap() *orderedmap.OrderedMap {
|
||||
m := orderedmap.New()
|
||||
for name, prop := range s.Properties {
|
||||
m.Set(name, prop)
|
||||
}
|
||||
m.SortKeys(sort.Strings)
|
||||
return m
|
||||
}
|
||||
|
||||
// HandleFieldAnnotations parses the comment (annotations look like `// @<KEY> [LANGUAGE] <VALUE>`)
|
||||
// and sets the schema properties
|
||||
func (s *JSONSchemaProps) HandleFieldAnnotations(comment string) {
|
||||
as := GetAnnotations(comment)
|
||||
for _, a := range as {
|
||||
switch a.Type {
|
||||
case ATitle:
|
||||
if s.Title == "" {
|
||||
s.Title = a.Text
|
||||
}
|
||||
s.XTitleI18n[a.I18nType] = a.Text
|
||||
case ADescription:
|
||||
if s.Description == "" {
|
||||
s.Description = a.Text
|
||||
}
|
||||
s.XDescriptionI18n[a.I18nType] = a.Text
|
||||
case AScope:
|
||||
s.Scope = Scope(a.Text)
|
||||
case AExample:
|
||||
s.Example = &JSON{Raw: []byte(a.Text)}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HandleFieldTags parses the struct field tags and sets the schema properties
|
||||
// TODO: Add more tags (now supported yaml, minimum, maximum, ...)
|
||||
func (s *JSONSchemaProps) HandleFieldTags(tags string, parent *JSONSchemaProps, fieldName string) string {
|
||||
if tags == "" {
|
||||
return fieldName
|
||||
}
|
||||
st, err := structtag.Parse(tags)
|
||||
if err != nil {
|
||||
return fieldName
|
||||
}
|
||||
|
||||
newName := fieldName
|
||||
for _, tag := range st.Tags() {
|
||||
switch tag.Key {
|
||||
case "yaml":
|
||||
newName = tag.Name
|
||||
if s.Title == "" {
|
||||
s.Title = newName
|
||||
s.XTitleI18n[I18nDefault] = newName
|
||||
}
|
||||
case "required":
|
||||
required, _ := strconv.ParseBool(tag.Name)
|
||||
if !required {
|
||||
continue
|
||||
}
|
||||
parent.Required = append(parent.Required, newName)
|
||||
case "minimum":
|
||||
min, err := strconv.ParseFloat(tag.Name, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s.Minimum = &min
|
||||
case "maximum":
|
||||
max, err := strconv.ParseFloat(tag.Name, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s.Maximum = &max
|
||||
case "minLength":
|
||||
minL, err := strconv.ParseInt(tag.Name, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s.MinLength = &minL
|
||||
case "maxLength":
|
||||
maxL, err := strconv.ParseInt(tag.Name, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s.MaxLength = &maxL
|
||||
case "minItems":
|
||||
minI, err := strconv.ParseInt(tag.Name, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s.MinItems = &minI
|
||||
case "maxItems":
|
||||
maxI, err := strconv.ParseInt(tag.Name, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s.MaxItems = &maxI
|
||||
case "pattern":
|
||||
s.Pattern = tag.Name
|
||||
}
|
||||
}
|
||||
|
||||
return newName
|
||||
}
|
||||
|
||||
// JoinRequirementsBy joins the requirements by the given i18n type. Return value looks like:
|
||||
// required, minLength 10, regular expression "^.*$"
|
||||
func (s *JSONSchemaProps) JoinRequirementsBy(i18n I18nType, required bool) string {
|
||||
reqs := s.getRequirements(required)
|
||||
switch i18n {
|
||||
case I18nZH_CN:
|
||||
return strings.Join(reqs[I18nZH_CN], ",")
|
||||
case I18nEN_US:
|
||||
fallthrough
|
||||
default:
|
||||
return strings.Join(reqs[I18nDefault], ", ")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *JSONSchemaProps) getRequirements(required bool) map[I18nType][]string {
|
||||
reqs := make(map[I18nType][]string)
|
||||
|
||||
for i18n, str := range s.GetRequired(required) {
|
||||
reqs[i18n] = append(reqs[i18n], str)
|
||||
}
|
||||
|
||||
for i18n, str := range s.GetMinimum() {
|
||||
reqs[i18n] = append(reqs[i18n], str)
|
||||
}
|
||||
|
||||
for i18n, str := range s.GetMaximum() {
|
||||
reqs[i18n] = append(reqs[i18n], str)
|
||||
}
|
||||
|
||||
for i18n, str := range s.GetMinLength() {
|
||||
reqs[i18n] = append(reqs[i18n], str)
|
||||
}
|
||||
|
||||
for i18n, str := range s.GetMaxLength() {
|
||||
reqs[i18n] = append(reqs[i18n], str)
|
||||
}
|
||||
|
||||
for i18n, str := range s.GetMinItems() {
|
||||
reqs[i18n] = append(reqs[i18n], str)
|
||||
}
|
||||
|
||||
for i18n, str := range s.GetMaxItems() {
|
||||
reqs[i18n] = append(reqs[i18n], str)
|
||||
}
|
||||
|
||||
for i18n, str := range s.GetPattern() {
|
||||
reqs[i18n] = append(reqs[i18n], str)
|
||||
}
|
||||
|
||||
return reqs
|
||||
}
|
||||
|
||||
func (s *JSONSchemaProps) GetMinimum() map[I18nType]string {
|
||||
if s.Minimum == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return map[I18nType]string{
|
||||
I18nZH_CN: fmt.Sprintf("最小值 %f", *s.Minimum),
|
||||
I18nEN_US: fmt.Sprintf("minimum %f", *s.Minimum),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *JSONSchemaProps) GetMaximum() map[I18nType]string {
|
||||
if s.Maximum == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return map[I18nType]string{
|
||||
I18nZH_CN: fmt.Sprintf("最大值 %f", *s.Maximum),
|
||||
I18nEN_US: fmt.Sprintf("maximum %f", *s.Maximum),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *JSONSchemaProps) GetMinLength() map[I18nType]string {
|
||||
if s.MinLength == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return map[I18nType]string{
|
||||
I18nZH_CN: fmt.Sprintf("最小长度 %d", *s.MinLength),
|
||||
I18nEN_US: fmt.Sprintf("minLength %d", *s.MinLength),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *JSONSchemaProps) GetMaxLength() map[I18nType]string {
|
||||
if s.MaxLength == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return map[I18nType]string{
|
||||
I18nZH_CN: fmt.Sprintf("最大长度 %d", *s.MaxLength),
|
||||
I18nEN_US: fmt.Sprintf("maxLength %d", *s.MaxLength),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *JSONSchemaProps) GetPattern() map[I18nType]string {
|
||||
if s.Pattern == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return map[I18nType]string{
|
||||
I18nZH_CN: fmt.Sprintf("正则表达式 %q", s.Pattern),
|
||||
I18nEN_US: fmt.Sprintf("regular expression %q", s.Pattern),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *JSONSchemaProps) GetMinItems() map[I18nType]string {
|
||||
if s.MinItems == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return map[I18nType]string{
|
||||
I18nZH_CN: fmt.Sprintf("最小 item 个数 %d", *s.MinItems),
|
||||
I18nEN_US: fmt.Sprintf("minItems %d", *s.MinItems),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *JSONSchemaProps) GetMaxItems() map[I18nType]string {
|
||||
if s.MaxItems == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return map[I18nType]string{
|
||||
I18nZH_CN: fmt.Sprintf("最大 item 个数 %d", *s.MaxItems),
|
||||
I18nEN_US: fmt.Sprintf("maxItems %d", *s.MaxItems),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *JSONSchemaProps) GetRequired(req bool) map[I18nType]string {
|
||||
if req {
|
||||
return map[I18nType]string{
|
||||
I18nZH_CN: "必填",
|
||||
I18nEN_US: "required",
|
||||
}
|
||||
}
|
||||
|
||||
return map[I18nType]string{
|
||||
I18nZH_CN: "选填",
|
||||
I18nEN_US: "optional",
|
||||
}
|
||||
}
|
||||
45
pkg/cmd/hgctl/plugin/types/testdata/doc_tag/main.go
vendored
Normal file
45
pkg/cmd/hgctl/plugin/types/testdata/doc_tag/main.go
vendored
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 main
|
||||
|
||||
// TestBasicDocTag This is a test struct for documents(comments) and tags
|
||||
type TestBasicDocTag struct {
|
||||
// Name, specify username
|
||||
Name string `yaml:"name" required:"true" minLength:"1" maxLength:"32"`
|
||||
|
||||
// Age, specify age
|
||||
Age uint `yaml:"age" required:"true" minimum:"0" maximum:"140" `
|
||||
|
||||
// Married, specify marital status [true, false]
|
||||
// and optional
|
||||
Married bool `yaml:"married" required:"false"`
|
||||
|
||||
// Salary, specify income status, optional
|
||||
Salary float64 `yaml:"salary" required:"false"`
|
||||
|
||||
// Children, specify a list of children's names, optional
|
||||
Children []string `yaml:"children" required:"false"`
|
||||
|
||||
// ignore1
|
||||
Ignore1 string `yaml:"-"`
|
||||
|
||||
// ignore 2
|
||||
Ignore2 string `yaml:""`
|
||||
}
|
||||
|
||||
type TestNestedStructDocTag struct {
|
||||
// This is the comment of the nested struct field
|
||||
Struct []*TestBasicDocTag `yaml:"struct" required:"true" minItems:"1" maxItems:"10"`
|
||||
}
|
||||
34
pkg/cmd/hgctl/plugin/types/testdata/types/ext/ext.go
vendored
Normal file
34
pkg/cmd/hgctl/plugin/types/testdata/types/ext/ext.go
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
// 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 ext
|
||||
|
||||
import "github.com/alibaba/higress/pkg/cmd/hgctl/plugin/types/testdata/types/ext/nested"
|
||||
|
||||
type TestExStruct struct {
|
||||
one string
|
||||
two *int
|
||||
three []bool
|
||||
}
|
||||
|
||||
type ExPointerInt **int
|
||||
type ExBool bool
|
||||
type ExSlice []*string
|
||||
type ExAlias nested.TestNestedStruct
|
||||
|
||||
type TestNestedStruct struct {
|
||||
NestedStruct *nested.TestNestedStruct
|
||||
NestedInt *nested.NestedInt
|
||||
NestedString nested.NestedString
|
||||
}
|
||||
23
pkg/cmd/hgctl/plugin/types/testdata/types/ext/nested/nested.go
vendored
Normal file
23
pkg/cmd/hgctl/plugin/types/testdata/types/ext/nested/nested.go
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
// 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 nested
|
||||
|
||||
type TestNestedStruct struct {
|
||||
Simple string
|
||||
Complex **[]*int
|
||||
}
|
||||
|
||||
type NestedInt ***int
|
||||
type NestedString string
|
||||
70
pkg/cmd/hgctl/plugin/types/testdata/types/main.go
vendored
Normal file
70
pkg/cmd/hgctl/plugin/types/testdata/types/main.go
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
// 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 main
|
||||
|
||||
import "github.com/alibaba/higress/pkg/cmd/hgctl/plugin/types/testdata/types/ext"
|
||||
|
||||
type TestBasicStruct struct {
|
||||
Name string
|
||||
Age uint
|
||||
Married bool
|
||||
Salary float64
|
||||
}
|
||||
|
||||
type TestComplexStruct struct {
|
||||
Array [2]int
|
||||
Slice []string
|
||||
Pointer *string
|
||||
PPPointer ***bool
|
||||
ArrayPointer [2]*int
|
||||
SlicePointer []*int
|
||||
StructPointerSlice []*TestBasicStruct
|
||||
StructArrayPointer *[]TestBasicStruct
|
||||
_ struct {
|
||||
one int
|
||||
two string
|
||||
}
|
||||
}
|
||||
|
||||
type TestAliasStruct struct {
|
||||
MyString *MyString
|
||||
MyPointerInt MyPointerInt
|
||||
MyStruct MyStruct
|
||||
}
|
||||
|
||||
type MyString string
|
||||
type MyPointerInt *int
|
||||
type MyStruct TestBasicStruct
|
||||
type NestedAlias ext.ExAlias
|
||||
type NestedBasicAlias ext.ExBool
|
||||
|
||||
type TestExternalStruct struct {
|
||||
InternalFloat float64
|
||||
ExStruct ext.TestExStruct
|
||||
ExternalInt ext.ExPointerInt
|
||||
ExBool ext.ExBool
|
||||
ExSlice ext.ExSlice
|
||||
}
|
||||
|
||||
type TestNestedStruct struct {
|
||||
NestedStruct *ext.TestNestedStruct
|
||||
}
|
||||
|
||||
type MyInterface interface {
|
||||
}
|
||||
|
||||
var MyConst bool
|
||||
|
||||
var MyVar int
|
||||
102
pkg/cmd/hgctl/plugin/uninstall/uninstall.go
Normal file
102
pkg/cmd/hgctl/plugin/uninstall/uninstall.go
Normal file
@@ -0,0 +1,102 @@
|
||||
// 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 uninstall
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
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"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
)
|
||||
|
||||
func NewCommand() *cobra.Command {
|
||||
var (
|
||||
name string
|
||||
all bool
|
||||
)
|
||||
|
||||
uninstallCmd := &cobra.Command{
|
||||
Use: "uninstall",
|
||||
Aliases: []string{"u", "uins"},
|
||||
Short: "Uninstall WASM plugin",
|
||||
Example: ` # Uninstall WASM plugin using the WasmPlugin name
|
||||
hgctl plugin uninstall -p example-plugin-name
|
||||
|
||||
# Uninstall all WASM plugins
|
||||
hgctl plugin uninstall -A
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmdutil.CheckErr(uninstall(cmd.OutOrStdout(), name, all))
|
||||
},
|
||||
}
|
||||
|
||||
flags := uninstallCmd.PersistentFlags()
|
||||
options.AddKubeConfigFlags(flags)
|
||||
k8s.AddHigressNamespaceFlags(flags)
|
||||
flags.StringVarP(&name, "name", "p", "", "Name of the WASM plugin you want to uninstall")
|
||||
flags.BoolVarP(&all, "all", "A", false, "Delete all installed WASM plugin")
|
||||
|
||||
return uninstallCmd
|
||||
}
|
||||
|
||||
func uninstall(w io.Writer, name string, all bool) error {
|
||||
dynCli, err := k8s.NewDynamicClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to build kubernetes dynamic client")
|
||||
}
|
||||
cli := k8s.NewWasmPluginClient(dynCli)
|
||||
|
||||
ctx := context.TODO()
|
||||
plugins := make([]string, 0)
|
||||
if all {
|
||||
list, err := cli.List(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get information of all wasm plugins")
|
||||
}
|
||||
for _, item := range list.Items {
|
||||
plugins = append(plugins, item.GetName())
|
||||
}
|
||||
} else {
|
||||
plugins = append(plugins, name)
|
||||
}
|
||||
|
||||
for _, p := range plugins {
|
||||
err = deleteOne(ctx, w, cli, p)
|
||||
if err != nil {
|
||||
fmt.Fprintln(w, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteOne(ctx context.Context, w io.Writer, cli *k8s.WasmPluginClient, name string) error {
|
||||
result, err := cli.Delete(ctx, name)
|
||||
if err != nil && k8serr.IsNotFound(err) {
|
||||
return errors.Errorf("wasm plugin %q is not found", fmt.Sprintf("%s/%s", k8s.HigressNamespace, name))
|
||||
} else if err != nil {
|
||||
return errors.Wrapf(err, "failed to uninstall wasm plugin %q", fmt.Sprintf("%s/%s", k8s.HigressNamespace, name))
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "Uninstalled wasm plugin %q\n", fmt.Sprintf("%s/%s", result.GetNamespace(), result.GetName()))
|
||||
return nil
|
||||
}
|
||||
92
pkg/cmd/hgctl/plugin/utils/common.go
Normal file
92
pkg/cmd/hgctl/plugin/utils/common.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 utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// GetAbsolutePath returns the absolute path, e.g.:
|
||||
// - ~/foo -> /home/user/foo
|
||||
// - ./foo -> /current/dir/foo
|
||||
// - /foo/ -> /foo
|
||||
func GetAbsolutePath(path string) (newPath string, err error) {
|
||||
if strings.HasPrefix(path, "~") {
|
||||
newPath, err = homedir.Expand(path)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to expand path: %q", path)
|
||||
}
|
||||
} else {
|
||||
newPath, err = filepath.Abs(path)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "failed to get absolute path of %q", path)
|
||||
}
|
||||
}
|
||||
|
||||
l := len(newPath)
|
||||
if l > 1 && newPath[l-1] == '/' { // if l == 1, the path might be "/"
|
||||
newPath = newPath[:l-1]
|
||||
}
|
||||
|
||||
return newPath, nil
|
||||
}
|
||||
|
||||
// AddIndent for each line of str
|
||||
func AddIndent(str, indent string) string {
|
||||
ret := ""
|
||||
ss := strings.Split(str, "\n")
|
||||
for i, s := range ss {
|
||||
if i == 0 {
|
||||
ret = fmt.Sprintf("%s%s", indent, s)
|
||||
} else {
|
||||
ret = fmt.Sprintf("%s\n%s%s", ret, indent, s)
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// MarshalYamlWithIndent marshals v to yaml with indent, specify space width with spaces
|
||||
func MarshalYamlWithIndent(v interface{}, spaces int) ([]byte, error) {
|
||||
w := new(bytes.Buffer)
|
||||
ec := yaml.NewEncoder(w)
|
||||
defer ec.Close()
|
||||
ec.SetIndent(spaces)
|
||||
if err := ec.Encode(v); err != nil {
|
||||
return w.Bytes(), err
|
||||
}
|
||||
|
||||
return w.Bytes(), nil
|
||||
}
|
||||
|
||||
// MarshalYamlWithIndentTo marshals v to yaml with indent, specify space width with spaces, and output to w
|
||||
func MarshalYamlWithIndentTo(w io.Writer, v interface{}, spaces int) error {
|
||||
ec := yaml.NewEncoder(w)
|
||||
defer ec.Close()
|
||||
ec.SetIndent(spaces)
|
||||
if err := ec.Encode(v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
58
pkg/cmd/hgctl/plugin/utils/debugger.go
Normal file
58
pkg/cmd/hgctl/plugin/utils/debugger.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 utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Debugger interface {
|
||||
Debugf(format string, a ...any) (int, error)
|
||||
Debugln(a ...any) (int, error)
|
||||
}
|
||||
|
||||
type DefaultDebugger struct {
|
||||
debug bool
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func NewDefaultDebugger(debug bool, w io.Writer) *DefaultDebugger {
|
||||
return &DefaultDebugger{debug: debug, w: w}
|
||||
}
|
||||
|
||||
func (d DefaultDebugger) Debugf(format string, a ...any) (int, error) {
|
||||
l := len(format)
|
||||
if l > 0 && format[l-1] != '\n' {
|
||||
format += "\n"
|
||||
}
|
||||
if d.debug {
|
||||
format = "[debug] " + format
|
||||
return fmt.Fprintf(d.w, format, a...)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (d DefaultDebugger) Debugln(a ...any) (int, error) {
|
||||
if d.debug {
|
||||
n1, err1 := fmt.Fprintf(d.w, "[debug] ")
|
||||
if err1 != nil {
|
||||
return n1, err1
|
||||
}
|
||||
n2, err2 := fmt.Fprintln(d.w, a...)
|
||||
return n1 + n2, err2
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
165
pkg/cmd/hgctl/plugin/utils/printer.go
Normal file
165
pkg/cmd/hgctl/plugin/utils/printer.go
Normal file
@@ -0,0 +1,165 @@
|
||||
// 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 utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
type YesOrNoPrinter struct {
|
||||
out io.Writer
|
||||
indent *Indent
|
||||
yes, no *color.Color
|
||||
}
|
||||
|
||||
var (
|
||||
DefaultOut = os.Stdout
|
||||
DefaultIdent = NewIndent(strings.Repeat(" ", 2), 0)
|
||||
DefaultYes = color.New(color.FgHiGreen)
|
||||
DefaultNo = color.New(color.FgHiRed)
|
||||
)
|
||||
|
||||
func NewPrinter(out io.Writer, indent *Indent, yes, no *color.Color) *YesOrNoPrinter {
|
||||
return &YesOrNoPrinter{
|
||||
out: out,
|
||||
indent: indent,
|
||||
yes: yes,
|
||||
no: no,
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultPrinter() *YesOrNoPrinter {
|
||||
return NewPrinter(DefaultOut, DefaultIdent, DefaultYes, DefaultNo)
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) Printf(format string, a ...interface{}) (int, error) {
|
||||
return fmt.Fprintf(p.out, format, a...)
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) Println(a ...interface{}) (int, error) {
|
||||
return fmt.Fprintln(p.out, a...)
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) PrintWithIndentf(format string, a ...interface{}) (int, error) {
|
||||
format = fmt.Sprintf("%s%s", p.indent, format)
|
||||
return fmt.Fprintf(p.out, format, a...)
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) PrintWithIndentln(a ...interface{}) (int, error) {
|
||||
n1, err := fmt.Fprintf(p.out, "%s", p.indent)
|
||||
if err != nil {
|
||||
return n1, err
|
||||
}
|
||||
n2, err := fmt.Fprintln(p.out, a...)
|
||||
if err != nil {
|
||||
return n1 + n2, err
|
||||
}
|
||||
return n1 + n2, nil
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) Yesf(format string, a ...interface{}) (int, error) {
|
||||
return p.yes.Fprintf(p.out, format, a...)
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) Yesln(a ...interface{}) (int, error) {
|
||||
return p.yes.Fprintln(p.out, a...)
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) YesWithIndentf(format string, a ...interface{}) (int, error) {
|
||||
format = fmt.Sprintf("%s%s", p.indent, format)
|
||||
return p.yes.Fprintf(p.out, format, a...)
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) YesWithIndentln(a ...interface{}) (int, error) {
|
||||
n1, err := p.yes.Fprintf(p.out, "%s", p.indent)
|
||||
if err != nil {
|
||||
return n1, err
|
||||
}
|
||||
n2, err := p.yes.Fprintln(p.out, a...)
|
||||
if err != nil {
|
||||
return n1 + n2, err
|
||||
}
|
||||
return n1 + n2, nil
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) Nof(format string, a ...interface{}) (int, error) {
|
||||
return p.no.Fprintf(p.out, format, a...)
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) Noln(a ...interface{}) (int, error) {
|
||||
return p.no.Fprintln(p.out, a...)
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) NoWithIndentf(format string, a ...interface{}) (int, error) {
|
||||
format = fmt.Sprintf("%s%s", p.indent, format)
|
||||
return p.no.Fprintf(p.out, format, a...)
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) NoWithIndentln(a ...interface{}) (int, error) {
|
||||
n1, err := p.no.Fprintf(p.out, "%s", p.indent)
|
||||
if err != nil {
|
||||
return n1, err
|
||||
}
|
||||
n2, err := p.no.Fprintln(p.out, a...)
|
||||
if err != nil {
|
||||
return n1 + n2, err
|
||||
}
|
||||
return n1 + n2, nil
|
||||
}
|
||||
|
||||
func (p *YesOrNoPrinter) Ident() string { return p.indent.String() }
|
||||
|
||||
func (p *YesOrNoPrinter) IncIdentRepeat() { p.indent.IncRepeat() }
|
||||
|
||||
func (p *YesOrNoPrinter) DecIndentRepeat() { p.indent.DecRepeat() }
|
||||
|
||||
func (p *YesOrNoPrinter) SetIdentRepeat(v int) { p.indent.SetRepeat(v) }
|
||||
|
||||
type Indent struct {
|
||||
format string
|
||||
repeat int
|
||||
}
|
||||
|
||||
func NewIndent(format string, repeat int) *Indent {
|
||||
return &Indent{
|
||||
format: format,
|
||||
repeat: repeat,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Indent) String() string {
|
||||
return strings.Repeat(i.format, i.repeat)
|
||||
}
|
||||
|
||||
func (i *Indent) IncRepeat() { i.repeat++ }
|
||||
|
||||
func (i *Indent) DecRepeat() {
|
||||
i.repeat--
|
||||
if i.repeat < 0 {
|
||||
i.repeat = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Indent) SetRepeat(v int) {
|
||||
if v < 0 {
|
||||
v = 0
|
||||
}
|
||||
i.repeat = v
|
||||
}
|
||||
31
pkg/cmd/hgctl/plugin/utils/survey_wrapper.go
Normal file
31
pkg/cmd/hgctl/plugin/utils/survey_wrapper.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// 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 utils
|
||||
|
||||
import "github.com/AlecAivazis/survey/v2"
|
||||
|
||||
func Ask(qs []*survey.Question, response interface{}, opts ...survey.AskOpt) error {
|
||||
opts = append(opts, survey.WithIcons(func(set *survey.IconSet) {
|
||||
set.Error.Format = "red+hb"
|
||||
}))
|
||||
return survey.Ask(qs, response, opts...)
|
||||
}
|
||||
|
||||
func AskOne(p survey.Prompt, response interface{}, opts ...survey.AskOpt) error {
|
||||
opts = append(opts, survey.WithIcons(func(set *survey.IconSet) {
|
||||
set.Error.Format = "red+hb"
|
||||
}))
|
||||
return survey.AskOne(p, response, opts...)
|
||||
}
|
||||
@@ -14,7 +14,10 @@
|
||||
|
||||
package hgctl
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
import (
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/plugin"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// GetRootCommand returns the root cobra command to be executed
|
||||
// by hgctl main.
|
||||
@@ -34,6 +37,7 @@ func GetRootCommand() *cobra.Command {
|
||||
rootCmd.AddCommand(newProfileCmd())
|
||||
rootCmd.AddCommand(newDashboardCmd())
|
||||
rootCmd.AddCommand(newManifestCmd())
|
||||
rootCmd.AddCommand(plugin.NewCommand())
|
||||
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
|
||||
"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"
|
||||
)
|
||||
@@ -29,18 +28,10 @@ import (
|
||||
type uninstallArgs struct {
|
||||
// purgeIstioCRD delete all of Istio resources.
|
||||
purgeIstioCRD bool
|
||||
// istioNamespace is the target namespace of istio control plane.
|
||||
istioNamespace string
|
||||
// namespace is the namespace of higress installed .
|
||||
namespace string
|
||||
}
|
||||
|
||||
func addUninstallFlags(cmd *cobra.Command, args *uninstallArgs) {
|
||||
cmd.PersistentFlags().StringVar(&args.istioNamespace, "istio-namespace", "istio-system",
|
||||
"The namespace of Istio Control Plane.")
|
||||
cmd.PersistentFlags().StringVarP(&args.namespace, "namespace", "n", "higress-system",
|
||||
"The namespace of higress")
|
||||
cmd.PersistentFlags().BoolVarP(&args.purgeIstioCRD, "purge-istio-crd", "p", false,
|
||||
cmd.PersistentFlags().BoolVarP(&args.purgeIstioCRD, "purge-istio-crd", "", false,
|
||||
"Delete all of Istio resources")
|
||||
}
|
||||
|
||||
@@ -50,15 +41,13 @@ func newUninstallCmd() *cobra.Command {
|
||||
uninstallCmd := &cobra.Command{
|
||||
Use: "uninstall",
|
||||
Short: "Uninstall higress from a cluster",
|
||||
Long: "The uninstall command uninstalls higress from a cluster",
|
||||
Long: "The uninstall command uninstalls higress from a cluster or local environment",
|
||||
Example: ` # Uninstall higress
|
||||
hgctl uninstall
|
||||
|
||||
# Uninstall higress by special namespace
|
||||
hgctl uninstall --namespace=higress-system
|
||||
hgctl uninstal
|
||||
|
||||
# Uninstall higress and istio CRD
|
||||
hgctl uninstall --purge-istio-crd --istio-namespace=istio-system`,
|
||||
# Uninstall higress and istio CRD from a cluster
|
||||
hgctl uninstall --purge-istio-crd
|
||||
`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return uninstall(cmd.OutOrStdout(), uiArgs)
|
||||
},
|
||||
@@ -71,24 +60,36 @@ func newUninstallCmd() *cobra.Command {
|
||||
|
||||
// uninstall uninstalls control plane by either pruning by target revision or deleting specified manifests.
|
||||
func uninstall(writer io.Writer, uiArgs *uninstallArgs) error {
|
||||
profileName, ok := installer.GetInstalledYamlPath()
|
||||
if !ok {
|
||||
fmt.Fprintf(writer, "\nHigress hasn't been installed yet!\n")
|
||||
return nil
|
||||
}
|
||||
setFlags := make([]string, 0)
|
||||
profileName := helm.GetUninstallProfileName()
|
||||
_, profile, err := helm.GenProfile(profileName, "", setFlags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(writer, "🧐 Validating Profile: \"%s\" \n", profileName)
|
||||
err = profile.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !promptUninstall(writer) {
|
||||
return nil
|
||||
}
|
||||
|
||||
profile.Global.EnableIstioAPI = uiArgs.purgeIstioCRD
|
||||
profile.Global.Namespace = uiArgs.namespace
|
||||
profile.Global.IstioNamespace = uiArgs.istioNamespace
|
||||
err = UnInstallManifests(profile, writer)
|
||||
if profile.Global.Install == helm.InstallK8s || profile.Global.Install == helm.InstallLocalK8s {
|
||||
profile.Global.EnableIstioAPI = uiArgs.purgeIstioCRD
|
||||
}
|
||||
|
||||
err = uninstallManifests(profile, writer, uiArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -108,29 +109,16 @@ func promptUninstall(writer io.Writer) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func UnInstallManifests(profile *helm.Profile, writer io.Writer) error {
|
||||
cliClient, err := kubernetes.NewCLIClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build kubernetes client: %w", err)
|
||||
}
|
||||
|
||||
op, err := installer.NewInstaller(profile, cliClient, writer, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := op.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifestMap, err := op.RenderManifests()
|
||||
func uninstallManifests(profile *helm.Profile, writer io.Writer, uiArgs *uninstallArgs) error {
|
||||
installer, err := installer.NewInstaller(profile, writer, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(writer, "\n⌛️ Processing uninstallation... \n\n")
|
||||
if err := op.DeleteManifests(manifestMap); err != nil {
|
||||
err = installer.UnInstall()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(writer, "\n🎊 Uninstall All Resources Complete!\n")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -15,6 +15,12 @@
|
||||
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"
|
||||
)
|
||||
@@ -24,6 +30,7 @@ type upgradeArgs struct {
|
||||
}
|
||||
|
||||
func addUpgradeFlags(cmd *cobra.Command, args *upgradeArgs) {
|
||||
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)
|
||||
}
|
||||
@@ -39,7 +46,7 @@ func newUpgradeCmd() *cobra.Command {
|
||||
Long: "The upgrade command is an alias for the install command" +
|
||||
" that performs additional upgrade-related checks.",
|
||||
RunE: func(cmd *cobra.Command, args []string) (e error) {
|
||||
return Install(cmd.OutOrStdout(), upgradeArgs.InstallArgs)
|
||||
return upgrade(cmd.OutOrStdout(), upgradeArgs.InstallArgs)
|
||||
},
|
||||
}
|
||||
addUpgradeFlags(upgradeCmd, upgradeArgs)
|
||||
@@ -47,3 +54,70 @@ func newUpgradeCmd() *cobra.Command {
|
||||
options.AddKubeConfigFlags(flags)
|
||||
return upgradeCmd
|
||||
}
|
||||
|
||||
// upgrade upgrade higress resources from the cluster.
|
||||
func upgrade(writer io.Writer, iArgs *InstallArgs) error {
|
||||
setFlags := applyFlagAliases(iArgs.Set, iArgs.ManifestsPath)
|
||||
profileName, ok := installer.GetInstalledYamlPath()
|
||||
if !ok {
|
||||
fmt.Fprintf(writer, "\nHigress hasn't been installed yet!\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
valuesOverlay, err := helm.GetValuesOverylayFromFiles(iArgs.InFilenames)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, profile, err := helm.GenProfile(profileName, valuesOverlay, setFlags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(writer, "🧐 Validating Profile: \"%s\" \n", profileName)
|
||||
err = profile.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !promptUpgrade(writer) {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = upgradeManifests(profile, writer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func promptUpgrade(writer io.Writer) bool {
|
||||
answer := ""
|
||||
for {
|
||||
fmt.Fprintf(writer, "All Higress resources will be upgraed from the cluster. \nProceed? (y/N)")
|
||||
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 upgradeManifests(profile *helm.Profile, writer io.Writer) error {
|
||||
installer, err := installer.NewInstaller(profile, writer, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = installer.Upgrade()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
123
pkg/cmd/hgctl/util/http_fetcher.go
Normal file
123
pkg/cmd/hgctl/util/http_fetcher.go
Normal file
@@ -0,0 +1,123 @@
|
||||
// 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 util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultInitialInterval = 500 * time.Millisecond
|
||||
defaultMaxInterval = 60 * time.Second
|
||||
)
|
||||
|
||||
type HTTPFetcher struct {
|
||||
client *http.Client
|
||||
initialBackoff time.Duration
|
||||
requestMaxRetry int
|
||||
bufferSize int64
|
||||
}
|
||||
|
||||
// NewHTTPFetcher create a new HTTP remote fetcher.
|
||||
func NewHTTPFetcher(requestTimeout time.Duration, requestMaxRetry int, bufferSize int64) *HTTPFetcher {
|
||||
if requestTimeout == 0 {
|
||||
requestTimeout = 5 * time.Second
|
||||
}
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
// nolint: gosec
|
||||
// This is only when a user explicitly sets a flag to enable insecure mode
|
||||
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
return &HTTPFetcher{
|
||||
client: &http.Client{
|
||||
Timeout: requestTimeout,
|
||||
},
|
||||
initialBackoff: defaultInitialInterval,
|
||||
requestMaxRetry: requestMaxRetry,
|
||||
bufferSize: bufferSize,
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch downloads with HTTP get.
|
||||
func (f *HTTPFetcher) Fetch(ctx context.Context, url string) ([]byte, error) {
|
||||
c := f.client
|
||||
delayInterval := f.initialBackoff
|
||||
attempts := 0
|
||||
var lastError error
|
||||
for attempts < f.requestMaxRetry {
|
||||
attempts++
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := c.Do(req)
|
||||
if err != nil {
|
||||
lastError = err
|
||||
if ctx.Err() != nil {
|
||||
// If there is context timeout, exit this loop.
|
||||
return nil, fmt.Errorf("download failed after %v attempts, last error: %v", attempts, lastError)
|
||||
}
|
||||
delayInterval = delayInterval + f.initialBackoff
|
||||
if delayInterval > defaultMaxInterval {
|
||||
break
|
||||
}
|
||||
time.Sleep(delayInterval)
|
||||
continue
|
||||
}
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, f.bufferSize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return body, err
|
||||
}
|
||||
|
||||
lastError = fmt.Errorf("download request failed: status code %v", resp.StatusCode)
|
||||
|
||||
if retryable(resp.StatusCode) {
|
||||
_, err := io.ReadAll(io.LimitReader(resp.Body, f.bufferSize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = resp.Body.Close()
|
||||
delayInterval = delayInterval + f.initialBackoff
|
||||
if delayInterval > defaultMaxInterval {
|
||||
break
|
||||
}
|
||||
time.Sleep(delayInterval)
|
||||
continue
|
||||
}
|
||||
|
||||
err = resp.Body.Close()
|
||||
break
|
||||
|
||||
}
|
||||
return nil, fmt.Errorf("download failed after %v attempts, last error: %v", attempts, lastError)
|
||||
}
|
||||
|
||||
func retryable(code int) bool {
|
||||
return code >= 500 &&
|
||||
!(code == http.StatusNotImplemented ||
|
||||
code == http.StatusHTTPVersionNotSupported ||
|
||||
code == http.StatusNetworkAuthenticationRequired)
|
||||
}
|
||||
@@ -15,8 +15,10 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@@ -76,3 +78,18 @@ func ParseValue(valueStr string) any {
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// WriteFileString write string content to file
|
||||
func WriteFileString(fileName string, content string, perm os.FileMode) error {
|
||||
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, perm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
writer := bufio.NewWriter(file)
|
||||
if _, err := writer.WriteString(content); err != nil {
|
||||
return err
|
||||
}
|
||||
writer.Flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -41,6 +41,8 @@ import (
|
||||
"istio.io/istio/pkg/config/constants"
|
||||
"istio.io/istio/pkg/config/schema/collection"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
ktypes "k8s.io/apimachinery/pkg/types"
|
||||
listersv1 "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
@@ -86,6 +88,10 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultMcpbridgeName = "default"
|
||||
)
|
||||
|
||||
type IngressConfig struct {
|
||||
// key: cluster id
|
||||
remoteIngressControllers map[string]common.IngressController
|
||||
@@ -155,7 +161,7 @@ func NewIngressConfig(localKubeClient kube.Client, XDSUpdater model.XDSUpdater,
|
||||
common.CreateConvertedName(clusterId, "global"),
|
||||
watchedSecretSet: sets.NewSet(),
|
||||
namespace: namespace,
|
||||
mcpbridgeReconciled: atomic.NewBool(true),
|
||||
mcpbridgeReconciled: atomic.NewBool(false),
|
||||
wasmPlugins: make(map[string]*extensions.WasmPlugin),
|
||||
http2rpcs: make(map[string]*higressv1.Http2Rpc),
|
||||
}
|
||||
@@ -939,7 +945,7 @@ func (m *IngressConfig) DeleteWasmPlugin(clusterNamespacedName util.ClusterNames
|
||||
|
||||
func (m *IngressConfig) AddOrUpdateMcpBridge(clusterNamespacedName util.ClusterNamespacedName) {
|
||||
// TODO: get resource name from config
|
||||
if clusterNamespacedName.Name != "default" || clusterNamespacedName.Namespace != m.namespace {
|
||||
if clusterNamespacedName.Name != DefaultMcpbridgeName || clusterNamespacedName.Namespace != m.namespace {
|
||||
return
|
||||
}
|
||||
mcpbridge, err := m.mcpbridgeLister.McpBridges(clusterNamespacedName.Namespace).Get(clusterNamespacedName.Name)
|
||||
@@ -948,7 +954,6 @@ func (m *IngressConfig) AddOrUpdateMcpBridge(clusterNamespacedName util.ClusterN
|
||||
clusterNamespacedName.Namespace, clusterNamespacedName.Name)
|
||||
return
|
||||
}
|
||||
m.mcpbridgeReconciled.Store(false)
|
||||
if m.RegistryReconciler == nil {
|
||||
m.RegistryReconciler = reconcile.NewReconciler(func() {
|
||||
metadata := config.Meta{
|
||||
@@ -1404,8 +1409,21 @@ func (m *IngressConfig) HasSynced() bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if !m.mcpbridgeController.HasSynced() || !m.mcpbridgeReconciled.Load() {
|
||||
if !m.mcpbridgeController.HasSynced() {
|
||||
return false
|
||||
} else {
|
||||
_, err := m.mcpbridgeController.Get(ktypes.NamespacedName{
|
||||
Namespace: m.namespace,
|
||||
Name: DefaultMcpbridgeName,
|
||||
})
|
||||
if err != nil {
|
||||
if !kerrors.IsNotFound(err) {
|
||||
return false
|
||||
}
|
||||
// mcpbridge exist
|
||||
} else if !m.mcpbridgeReconciled.Load() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if !m.wasmPluginController.HasSynced() {
|
||||
return false
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user