mirror of
https://github.com/alibaba/higress.git
synced 2026-02-26 05:30:50 +08:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
792b9b0ee5 | ||
|
|
26ed9a6d93 | ||
|
|
ed36a4989f | ||
|
|
f23e26374f | ||
|
|
eb2934c084 | ||
|
|
2da1c62c69 | ||
|
|
fab734d39a | ||
|
|
2393af5c85 | ||
|
|
b142f51776 | ||
|
|
587267a733 | ||
|
|
a2078711f5 | ||
|
|
dc54c581f3 | ||
|
|
b47d74bce5 | ||
|
|
8d8ad6d624 | ||
|
|
8062625d75 | ||
|
|
54a8a906ae | ||
|
|
8659895a91 | ||
|
|
dc3e496aa0 | ||
|
|
8747e1ddad | ||
|
|
2b9e3a14c2 | ||
|
|
1051201e97 | ||
|
|
8b24a20651 |
6
.github/workflows/build-and-test.yaml
vendored
6
.github/workflows/build-and-test.yaml
vendored
@@ -147,6 +147,10 @@ jobs:
|
||||
higress-wasmplugin-test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build]
|
||||
strategy:
|
||||
matrix:
|
||||
# TODO(Xunzhuo): Enable C WASM Filters in CI
|
||||
wasmPluginType: [ GO ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
@@ -179,7 +183,7 @@ jobs:
|
||||
- run: git stash # restore patch
|
||||
|
||||
- name: "Run Ingress WasmPlugins Tests"
|
||||
run: GOPROXY="https://proxy.golang.org,direct" make higress-wasmplugin-test
|
||||
run: GOPROXY="https://proxy.golang.org,direct" PLUGIN_TYPE=${{ matrix.wasmPluginType }} make higress-wasmplugin-test
|
||||
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -151,10 +151,16 @@ build-pilot:
|
||||
build-gateway: prebuild external/package/envoy-amd64.tar.gz external/package/envoy-arm64.tar.gz build-pilot
|
||||
cd external/istio; BUILD_WITH_CONTAINER=1 BUILDX_PLATFORM=true DOCKER_BUILD_VARIANTS=default DOCKER_TARGETS="docker.proxyv2" make docker
|
||||
|
||||
build-istio: prebuild build-pilot
|
||||
cd external/istio; BUILD_WITH_CONTAINER=1 BUILDX_PLATFORM=true DOCKER_BUILD_VARIANTS=default DOCKER_TARGETS="docker.pilot" make docker
|
||||
build-gateway-local: prebuild external/package/envoy-amd64.tar.gz external/package/envoy-arm64.tar.gz build-pilot
|
||||
cd external/istio; BUILD_WITH_CONTAINER=1 BUILDX_PLATFORM=false DOCKER_BUILD_VARIANTS=default DOCKER_TARGETS="docker.proxyv2" make docker
|
||||
|
||||
build-wasmplugins:
|
||||
build-istio: prebuild build-pilot
|
||||
cd external/istio; BUILD_WITH_CONTAINER=1 BUILDX_PLATFORM=true DOCKER_BUILD_VARIANTS=default DOCKER_TARGETS="docker.pilot" make docker
|
||||
|
||||
build-istio-local: prebuild build-pilot
|
||||
cd external/istio; BUILD_WITH_CONTAINER=1 BUILDX_PLATFORM=false DOCKER_BUILD_VARIANTS=default DOCKER_TARGETS="docker.pilot" make docker
|
||||
|
||||
build-wasmplugins:
|
||||
./tools/hack/build-wasm-plugins.sh
|
||||
|
||||
pre-install:
|
||||
@@ -168,14 +174,13 @@ install: pre-install
|
||||
cd helm/higress; helm dependency build
|
||||
helm install higress helm/higress -n higress-system --create-namespace --set 'global.local=true'
|
||||
|
||||
ENVOY_LATEST_IMAGE_TAG ?= 1.1.1
|
||||
ISTIO_LATEST_IMAGE_TAG ?= 1.1.1
|
||||
ENVOY_LATEST_IMAGE_TAG ?= 1.2.0
|
||||
ISTIO_LATEST_IMAGE_TAG ?= 1.2.0
|
||||
|
||||
install-dev: pre-install
|
||||
helm install higress helm/core -n higress-system --create-namespace --set 'controller.tag=$(TAG)' --set 'gateway.replicas=1' --set 'gateway.tag=$(ENVOY_LATEST_IMAGE_TAG)' --set 'global.local=true'
|
||||
|
||||
helm install higress helm/core -n higress-system --create-namespace --set 'controller.tag=$(TAG)' --set 'gateway.replicas=1' --set 'pilot.tag=$(ISTIO_LATEST_IMAGE_TAG)' --set 'gateway.tag=$(ENVOY_LATEST_IMAGE_TAG)' --set 'global.local=true'
|
||||
install-dev-wasmplugin: build-wasmplugins pre-install
|
||||
helm install higress helm/core -n higress-system --create-namespace --set 'controller.tag=$(TAG)' --set 'gateway.replicas=1' --set 'gateway.tag=$(ENVOY_LATEST_IMAGE_TAG)' --set 'global.local=true' --set 'global.volumeWasmPlugins=true'
|
||||
helm install higress helm/core -n higress-system --create-namespace --set 'controller.tag=$(TAG)' --set 'gateway.replicas=1' --set 'pilot.tag=$(ISTIO_LATEST_IMAGE_TAG)' --set 'gateway.tag=$(ENVOY_LATEST_IMAGE_TAG)' --set 'global.local=true' --set 'global.volumeWasmPlugins=true'
|
||||
|
||||
uninstall:
|
||||
helm uninstall higress -n higress-system
|
||||
|
||||
81
go.mod
81
go.mod
@@ -43,17 +43,17 @@ require (
|
||||
istio.io/gogo-genproto v0.0.0-20211115195057-0e34bdd2be67
|
||||
istio.io/istio v0.0.0
|
||||
istio.io/pkg v0.0.0-20211115195056-e379f31ee62a
|
||||
k8s.io/api v0.22.2
|
||||
k8s.io/apimachinery v0.22.2
|
||||
k8s.io/api v0.22.5
|
||||
k8s.io/apimachinery v0.22.5
|
||||
k8s.io/cli-runtime v0.22.2
|
||||
k8s.io/client-go v0.22.2
|
||||
k8s.io/client-go v0.22.5
|
||||
k8s.io/kubectl v0.22.2
|
||||
sigs.k8s.io/controller-runtime v0.10.2
|
||||
sigs.k8s.io/yaml v1.3.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.97.0 // indirect
|
||||
cloud.google.com/go v0.98.0 // indirect
|
||||
cloud.google.com/go/logging v1.4.2 // indirect
|
||||
contrib.go.opencensus.io/exporter/prometheus v0.4.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
@@ -63,37 +63,47 @@ require (
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
|
||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||
github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.2 // indirect
|
||||
github.com/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/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/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/jsonparser v1.1.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.1 // 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/coreos/go-oidc/v3 v3.1.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0 // indirect
|
||||
github.com/docker/cli v20.10.7+incompatible // indirect
|
||||
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||
github.com/docker/docker v20.10.7+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.6.3 // indirect
|
||||
github.com/docker/go-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/envoyproxy/protoc-gen-validate v0.1.0 // indirect
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
|
||||
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
|
||||
github.com/fatih/color v1.14.1 // indirect
|
||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 // indirect
|
||||
@@ -103,10 +113,11 @@ require (
|
||||
github.com/go-errors/errors v1.0.1 // indirect
|
||||
github.com/go-kit/log v0.1.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.5.0 // indirect
|
||||
github.com/go-logr/logr v0.4.0 // indirect
|
||||
github.com/go-logr/logr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.5 // indirect
|
||||
github.com/go-openapi/swag v0.19.14 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/goccy/go-json v0.4.8 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
@@ -118,6 +129,8 @@ require (
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.1.1 // indirect
|
||||
github.com/googleapis/gnostic v0.5.5 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/gosuri/uitable v0.0.4 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
@@ -132,9 +145,13 @@ require (
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/jmoiron/sqlx v1.3.1 // indirect
|
||||
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||
github.com/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/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.7 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.0 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.0 // indirect
|
||||
@@ -143,10 +160,12 @@ require (
|
||||
github.com/lestrrat-go/option v1.0.0 // indirect
|
||||
github.com/lestrrat/go-file-rotatelogs v0.0.0-20180223000712-d3151e2a480f // indirect
|
||||
github.com/lestrrat/go-strftime v0.0.0-20180220042222-ba3bf9c1d042 // indirect
|
||||
github.com/lib/pq v1.10.0 // indirect
|
||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/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/miekg/dns v1.1.43 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
@@ -154,11 +173,13 @@ require (
|
||||
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/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/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible // indirect
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
@@ -172,12 +193,17 @@ require (
|
||||
github.com/prometheus/common v0.32.1 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/prometheus/statsd_exporter v0.21.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc // indirect
|
||||
github.com/russross/blackfriday v1.5.2 // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/toolkits/concurrent v0.0.0-20150624120057-a4371d70e3e3 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect
|
||||
github.com/yl2chen/cidranger v1.0.2 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
@@ -197,21 +223,23 @@ require (
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v3 v3.0.1 // indirect
|
||||
gomodules.xyz/orderedmap v0.1.0 // indirect
|
||||
google.golang.org/api v0.59.0 // indirect
|
||||
google.golang.org/api v0.61.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20211020151524-b7c3a969101a // indirect
|
||||
google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12 // indirect
|
||||
gopkg.in/gcfg.v1 v1.2.3 // indirect
|
||||
gopkg.in/gorp.v1 v1.7.2 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.66.2 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.22.2 // indirect
|
||||
k8s.io/apiserver v0.22.2 // indirect
|
||||
k8s.io/component-base v0.22.2 // indirect
|
||||
k8s.io/klog/v2 v2.10.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20211020163157-7327e2aaee2b // indirect
|
||||
k8s.io/klog/v2 v2.40.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c // indirect
|
||||
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect
|
||||
oras.land/oras-go v0.4.0 // indirect
|
||||
sigs.k8s.io/gateway-api v0.4.0 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.8.11 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.11.0 // indirect
|
||||
@@ -230,3 +258,30 @@ replace istio.io/pkg => ./external/pkg
|
||||
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
|
||||
github.com/kylelemons/godebug v1.1.0
|
||||
helm.sh/helm/v3 v3.7.1
|
||||
k8s.io/apiextensions-apiserver v0.25.4
|
||||
knative.dev/networking v0.0.0-20220302134042-e8b2eb995165
|
||||
knative.dev/pkg v0.0.0-20220301181942-2fdd5f232e77
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 1.1.1
|
||||
appVersion: 1.2.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.1.1
|
||||
version: 1.2.0
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
configSources:
|
||||
- address: "xds://127.0.0.1:15051"
|
||||
{{- if .Values.global.enableIstioAPI }}
|
||||
{{- if or .Values.global.enableIstioAPI .Values.global.enableGatewayAPI }}
|
||||
- address: "k8s://"
|
||||
{{- end }}
|
||||
|
||||
|
||||
@@ -117,3 +117,10 @@ rules:
|
||||
- apiGroups: ["config.istio.io", "security.istio.io", "networking.istio.io", "authentication.istio.io", "rbac.istio.io", "telemetry.istio.io", "extensions.istio.io"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
resources: ["*"]
|
||||
# knative KIngress configuration
|
||||
- apiGroups: ["networking.internal.knative.dev"]
|
||||
verbs: ["get","list","watch"]
|
||||
resources: ["ingresses"]
|
||||
- apiGroups: ["networking.internal.knative.dev"]
|
||||
resources: ["ingresses/status"]
|
||||
verbs: ["get","patch","update"]
|
||||
@@ -33,7 +33,7 @@ spec:
|
||||
{{- if contains "/" .Values.pilot.image }}
|
||||
image: "{{ .Values.pilot.image }}"
|
||||
{{- else }}
|
||||
image: "{{ .Values.pilot.hub | default .Values.global.hub }}/{{ .Values.pilot.image | default "pilot" }}:{{ .Values.pilot.tag | default .Values.global.tag }}"
|
||||
image: "{{ .Values.pilot.hub | default .Values.global.hub }}/{{ .Values.pilot.image | default "pilot" }}:{{ .Values.pilot.tag | default .Chart.AppVersion }}"
|
||||
{{- end }}
|
||||
{{- if .Values.global.imagePullPolicy }}
|
||||
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
|
||||
@@ -126,10 +126,19 @@ spec:
|
||||
value: "{{ .Values.global.istiod.enableAnalysis }}"
|
||||
- name: CLUSTER_ID
|
||||
value: "{{ $.Values.global.multiCluster.clusterName | default `Kubernetes` }}"
|
||||
# HIGRESS_ENABLE_ISTIO_API is only used to restart the controller pod after the config change
|
||||
{{- if .Values.global.enableIstioAPI }}
|
||||
- name: HIGRESS_ENABLE_ISTIO_API
|
||||
value: "true"
|
||||
{{- end }}
|
||||
{{- if .Values.global.enableGatewayAPI }}
|
||||
- name: PILOT_ENABLE_GATEWAY_API
|
||||
value: "true"
|
||||
- name: PILOT_ENABLE_GATEWAY_API_STATUS
|
||||
value: "true"
|
||||
- name: PILOT_ENABLE_GATEWAY_API_DEPLOYMENT_CONTROLLER
|
||||
value: "false"
|
||||
{{- end }}
|
||||
{{- if not .Values.global.enableHigressIstio }}
|
||||
- name: CUSTOM_CA_CERT_NAME
|
||||
value: "higress-ca-root-cert"
|
||||
|
||||
@@ -17,6 +17,7 @@ global:
|
||||
local: false # When deploying to a local cluster (e.g.: kind cluster), set this to true.
|
||||
kind: false # Deprecated. Please use "global.local" instead. Will be removed later.
|
||||
enableIstioAPI: false
|
||||
enableGatewayAPI: false
|
||||
# Deprecated
|
||||
enableHigressIstio: false
|
||||
# Used to locate istiod.
|
||||
@@ -44,8 +45,6 @@ global:
|
||||
# Releases are published to docker hub under 'istio' project.
|
||||
# Dev builds from prow are on gcr.io
|
||||
hub: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress
|
||||
# Default tag for Istio images.
|
||||
tag: 1.1.1
|
||||
|
||||
# Specify image pull policy if default behavior isn't desired.
|
||||
# Default behavior: latest images will be Always else IfNotPresent.
|
||||
@@ -369,7 +368,7 @@ gateway:
|
||||
name: "higress-gateway"
|
||||
replicas: 2
|
||||
image: gateway
|
||||
tag: "1.1.1"
|
||||
tag: ""
|
||||
# revision declares which revision this gateway is a part of
|
||||
revision: ""
|
||||
|
||||
@@ -457,7 +456,7 @@ controller:
|
||||
name: "higress-controller"
|
||||
replicas: 1
|
||||
image: higress
|
||||
tag: "1.1.1"
|
||||
tag: ""
|
||||
env: {}
|
||||
|
||||
labels: {}
|
||||
@@ -547,7 +546,7 @@ pilot:
|
||||
rollingMaxUnavailable: 25%
|
||||
|
||||
hub: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress
|
||||
tag: 1.1.1
|
||||
tag: ""
|
||||
|
||||
# Can be a full hub/image:tag
|
||||
image: pilot
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
dependencies:
|
||||
- name: higress-core
|
||||
repository: file://../core
|
||||
version: 1.1.1
|
||||
version: 1.2.0
|
||||
- name: higress-console
|
||||
repository: https://higress.io/helm-charts/
|
||||
version: 1.1.1
|
||||
digest: sha256:dd74a69c4031fa3e7798233602b44f0da3f657cbb40c61754298fbc877be2ae6
|
||||
generated: "2023-08-10T10:54:46.8520756+08:00"
|
||||
version: 1.2.0
|
||||
digest: sha256:d53c2da70cb3bcace50bce756acb50750a84c0888ec0d4c112939bf3c6e4daeb
|
||||
generated: "2023-09-22T16:52:34.940675+08:00"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 1.1.1
|
||||
appVersion: 1.2.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.1.1
|
||||
version: 1.2.0
|
||||
- name: higress-console
|
||||
repository: "https://higress.io/helm-charts/"
|
||||
version: 1.1.1
|
||||
version: 1.2.0
|
||||
type: application
|
||||
version: 1.1.1
|
||||
version: 1.2.0
|
||||
|
||||
@@ -40,6 +40,7 @@ The command removes all the Kubernetes components associated with the chart and
|
||||
| global.disableAlpnH2 | Whether to disable HTTP/2 in ALPN | true |
|
||||
| global.enableStatus | If `true`, Higress Controller will update the `status` field of Ingress resources.<br />When migrating from Nginx Ingress, in order to avoid `status` field of Ingress objects being overwritten, this parameter needs to be set to false, so Higress won't write the entry IP to the `status` field of the corresponding Ingress object. | true |
|
||||
| global.enableIstioAPI | If `true`, Higress Controller will monitor istio resources as well | false |
|
||||
| global.enableGatewayAPI | If `true`, Higress Controller will monitor Gateway API resources as well | false |
|
||||
| global.istioNamespace | The namespace istio is installed to | istio-system |
|
||||
| **Core Paramters** | | |
|
||||
| higress-core.gateway.replicas | Number of Higress Gateway pods | 2 |
|
||||
|
||||
14
istio/1.12/patches/istio/20230922-gateway-class.patch
Normal file
14
istio/1.12/patches/istio/20230922-gateway-class.patch
Normal file
@@ -0,0 +1,14 @@
|
||||
diff -Naur istio/pilot/pkg/config/kube/gateway/conversion.go istio_new/pilot/pkg/config/kube/gateway/conversion.go
|
||||
--- istio/pilot/pkg/config/kube/gateway/conversion.go 2023-09-22 11:06:50.400535200 +0800
|
||||
+++ istio_new/pilot/pkg/config/kube/gateway/conversion.go 2023-09-22 11:07:52.954982700 +0800
|
||||
@@ -37,8 +37,8 @@
|
||||
)
|
||||
|
||||
const (
|
||||
- DefaultClassName = "istio"
|
||||
- ControllerName = "istio.io/gateway-controller"
|
||||
+ DefaultClassName = "higress"
|
||||
+ ControllerName = "higress.io/gateway-controller"
|
||||
)
|
||||
|
||||
// KubernetesResources stores all inputs to our conversion
|
||||
@@ -20,6 +20,10 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/common"
|
||||
"github.com/alibaba/higress/pkg/ingress/mcp"
|
||||
"github.com/alibaba/higress/pkg/ingress/translation"
|
||||
higresskube "github.com/alibaba/higress/pkg/kube"
|
||||
prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/reflection"
|
||||
@@ -46,11 +50,6 @@ import (
|
||||
"istio.io/pkg/log"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
ingressconfig "github.com/alibaba/higress/pkg/ingress/config"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/common"
|
||||
"github.com/alibaba/higress/pkg/ingress/mcp"
|
||||
higresskube "github.com/alibaba/higress/pkg/kube"
|
||||
)
|
||||
|
||||
type XdsOptions struct {
|
||||
@@ -225,9 +224,12 @@ func (s *Server) initConfigController() error {
|
||||
if options.ClusterId == "Kubernetes" {
|
||||
options.ClusterId = ""
|
||||
}
|
||||
ingressConfig := ingressconfig.NewIngressConfig(s.kubeClient, s.xdsServer, ns, options.ClusterId)
|
||||
ingressController := ingressConfig.AddLocalCluster(options)
|
||||
|
||||
ingressConfig := translation.NewIngressTranslation(s.kubeClient, s.xdsServer, ns, options.ClusterId)
|
||||
ingressController, kingressController := ingressConfig.AddLocalCluster(options)
|
||||
|
||||
s.configStores = append(s.configStores, ingressConfig)
|
||||
|
||||
// Wrap the config controller with a cache.
|
||||
aggregateConfigController, err := configaggregate.MakeCache(s.configStores)
|
||||
if err != nil {
|
||||
@@ -242,7 +244,7 @@ func (s *Server) initConfigController() error {
|
||||
|
||||
// Defer starting the controller until after the service is created.
|
||||
s.server.RunComponent(func(stop <-chan struct{}) error {
|
||||
if err := ingressConfig.InitializeCluster(ingressController, stop); err != nil {
|
||||
if err := ingressConfig.InitializeCluster(ingressController, kingressController, stop); err != nil {
|
||||
return err
|
||||
}
|
||||
go s.configController.Run(stop)
|
||||
|
||||
@@ -12,8 +12,10 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tests
|
||||
package hgctl
|
||||
|
||||
import "github.com/alibaba/higress/test/e2e/conformance/utils/suite"
|
||||
|
||||
var HigressConformanceTests []suite.ConformanceTest
|
||||
const (
|
||||
yamlOutput = "yaml"
|
||||
jsonOutput = "json"
|
||||
flagsOutput = "flags"
|
||||
)
|
||||
@@ -33,8 +33,8 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
adminPort = 15000
|
||||
containerName = "envoy"
|
||||
defaultProxyAdminPort = 15000
|
||||
containerName = "envoy"
|
||||
)
|
||||
|
||||
func retrieveConfigDump(args []string, includeEds bool) ([]byte, error) {
|
||||
@@ -96,7 +96,7 @@ func portForwarder(nn types.NamespacedName) (kubernetes.PortForwarder, error) {
|
||||
return nil, fmt.Errorf("pod %s is not running", nn)
|
||||
}
|
||||
|
||||
fw, err := kubernetes.NewLocalPortForwarder(c, nn, 0, adminPort)
|
||||
fw, err := kubernetes.NewLocalPortForwarder(c, nn, 0, defaultProxyAdminPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -34,6 +34,7 @@ type fakePortForwarder struct {
|
||||
localPort int
|
||||
l net.Listener
|
||||
mux *http.ServeMux
|
||||
stopCh chan struct{}
|
||||
}
|
||||
|
||||
func newFakePortForwarder(b []byte) (kubernetes.PortForwarder, error) {
|
||||
@@ -46,6 +47,7 @@ func newFakePortForwarder(b []byte) (kubernetes.PortForwarder, error) {
|
||||
responseBody: b,
|
||||
localPort: p,
|
||||
mux: http.NewServeMux(),
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
fw.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write(fw.responseBody)
|
||||
@@ -54,6 +56,10 @@ func newFakePortForwarder(b []byte) (kubernetes.PortForwarder, error) {
|
||||
return fw, nil
|
||||
}
|
||||
|
||||
func (fw *fakePortForwarder) WaitForStop() {
|
||||
<-fw.stopCh
|
||||
}
|
||||
|
||||
func (fw *fakePortForwarder) Start() error {
|
||||
l, err := net.Listen("tcp", fw.Address())
|
||||
if err != nil {
|
||||
|
||||
345
pkg/cmd/hgctl/dashboard.go
Normal file
345
pkg/cmd/hgctl/dashboard.go
Normal file
@@ -0,0 +1,345 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hgctl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
|
||||
"github.com/alibaba/higress/pkg/cmd/options"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
)
|
||||
|
||||
var (
|
||||
listenPort = 0
|
||||
promPort = 0
|
||||
grafanaPort = 0
|
||||
consolePort = 0
|
||||
|
||||
bindAddress = "localhost"
|
||||
|
||||
// open browser or not, default is true
|
||||
browser = true
|
||||
|
||||
// label selector
|
||||
labelSelector = ""
|
||||
|
||||
addonNamespace = ""
|
||||
|
||||
envoyDashNs = ""
|
||||
|
||||
proxyAdminPort int
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPrometheusPort = 9090
|
||||
defaultGrafanaPort = 3000
|
||||
defaultConsolePort = 8080
|
||||
)
|
||||
|
||||
func newDashboardCmd() *cobra.Command {
|
||||
dashboardCmd := &cobra.Command{
|
||||
Use: "dashboard",
|
||||
Aliases: []string{"dash", "d"},
|
||||
Short: "Access to Higress web UIs",
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 0 {
|
||||
return fmt.Errorf("unknown dashboard %q", args[0])
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cmd.HelpFunc()(cmd, args)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
dashboardCmd.PersistentFlags().IntVarP(&listenPort, "port", "p", 0, "Local port to listen to")
|
||||
dashboardCmd.PersistentFlags().BoolVar(&browser, "browser", true,
|
||||
"When --browser is supplied as false, hgctl dashboard will not open the browser. "+
|
||||
"Default is true which means hgctl dashboard will always open a browser to view the dashboard.")
|
||||
dashboardCmd.PersistentFlags().StringVarP(&addonNamespace, "namespace", "n", "higress-system",
|
||||
"Namespace where the addon is running, if not specified, higress-system would be used")
|
||||
|
||||
prom := promDashCmd()
|
||||
prom.PersistentFlags().IntVar(&promPort, "ui-port", defaultPrometheusPort, "The component dashboard UI port.")
|
||||
dashboardCmd.AddCommand(prom)
|
||||
|
||||
graf := grafanaDashCmd()
|
||||
graf.PersistentFlags().IntVar(&grafanaPort, "ui-port", defaultGrafanaPort, "The component dashboard UI port.")
|
||||
dashboardCmd.AddCommand(graf)
|
||||
|
||||
envoy := envoyDashCmd()
|
||||
envoy.PersistentFlags().StringVarP(&labelSelector, "selector", "l", "app=higress-gateway", "Label selector")
|
||||
envoy.PersistentFlags().StringVarP(&envoyDashNs, "namespace", "n", "",
|
||||
"Namespace where the addon is running, if not specified, higress-system would be used")
|
||||
envoy.PersistentFlags().IntVar(&proxyAdminPort, "ui-port", defaultProxyAdminPort, "The component dashboard UI port.")
|
||||
dashboardCmd.AddCommand(envoy)
|
||||
|
||||
consoleCmd := consoleDashCmd()
|
||||
consoleCmd.PersistentFlags().IntVar(&consolePort, "ui-port", defaultConsolePort, "The component dashboard UI port.")
|
||||
dashboardCmd.AddCommand(consoleCmd)
|
||||
|
||||
return dashboardCmd
|
||||
}
|
||||
|
||||
// port-forward to Higress System Prometheus; open browser
|
||||
func promDashCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "prometheus",
|
||||
Short: "Open Prometheus web UI",
|
||||
Long: `Open Higress's Prometheus dashboard`,
|
||||
Example: ` hgctl dashboard prometheus
|
||||
|
||||
# with short syntax
|
||||
hgctl dash prometheus
|
||||
hgctl d prometheus`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := kubernetes.NewCLIClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
|
||||
if err != nil {
|
||||
return fmt.Errorf("build CLI client fail: %w", err)
|
||||
}
|
||||
|
||||
pl, err := client.PodsForSelector(addonNamespace, "app=higress-console-prometheus")
|
||||
if err != nil {
|
||||
return fmt.Errorf("not able to locate Prometheus pod: %v", err)
|
||||
}
|
||||
|
||||
if len(pl.Items) < 1 {
|
||||
return errors.New("no Prometheus pods found")
|
||||
}
|
||||
|
||||
// only use the first pod in the list
|
||||
return portForward(pl.Items[0].Name, addonNamespace, "Prometheus",
|
||||
"http://%s", bindAddress, promPort, client, cmd.OutOrStdout(), browser)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// port-forward to Higress System Console; open browser
|
||||
func consoleDashCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "console",
|
||||
Short: "Open Console web UI",
|
||||
Long: `Open Higress Console`,
|
||||
Example: ` hgctl dashboard console
|
||||
|
||||
# with short syntax
|
||||
hgctl dash console
|
||||
hgctl d console`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := kubernetes.NewCLIClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
|
||||
if err != nil {
|
||||
return fmt.Errorf("build CLI client fail: %w", err)
|
||||
}
|
||||
|
||||
pl, err := client.PodsForSelector(addonNamespace, "app.kubernetes.io/name=higress-console")
|
||||
if err != nil {
|
||||
return fmt.Errorf("not able to locate console pod: %v", err)
|
||||
}
|
||||
|
||||
if len(pl.Items) < 1 {
|
||||
return errors.New("no higress console pods found")
|
||||
}
|
||||
|
||||
// only use the first pod in the list
|
||||
return portForward(pl.Items[0].Name, addonNamespace, "Console",
|
||||
"http://%s", bindAddress, consolePort, client, cmd.OutOrStdout(), browser)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// port-forward to Higress System Grafana; open browser
|
||||
func grafanaDashCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "grafana",
|
||||
Short: "Open Grafana web UI",
|
||||
Long: `Open Higress's Grafana dashboard`,
|
||||
Example: ` hgctl dashboard grafana
|
||||
|
||||
# with short syntax
|
||||
hgctl dash grafana
|
||||
hgctl d grafana`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, err := kubernetes.NewCLIClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
|
||||
if err != nil {
|
||||
return fmt.Errorf("build CLI client fail: %w", err)
|
||||
}
|
||||
pl, err := client.PodsForSelector(addonNamespace, "app=higress-console-grafana")
|
||||
if err != nil {
|
||||
return fmt.Errorf("not able to locate Grafana pod: %v", err)
|
||||
}
|
||||
|
||||
if len(pl.Items) < 1 {
|
||||
return errors.New("no Grafana pods found")
|
||||
}
|
||||
|
||||
// only use the first pod in the list
|
||||
return portForward(pl.Items[0].Name, addonNamespace, "Grafana",
|
||||
"http://%s", bindAddress, grafanaPort, client, cmd.OutOrStdout(), browser)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// port-forward to sidecar Envoy admin port; open browser
|
||||
func envoyDashCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "envoy [<type>/]<name>[.<namespace>]",
|
||||
Short: "Open Envoy admin web UI",
|
||||
Long: `Open the Envoy admin dashboard for a higress gateway`,
|
||||
Example: ` # Open Envoy dashboard for the higress-gateway-56f9b9797-b9nnc
|
||||
hgctl dashboard envoy higress-gateway-56f9b9797-b9nnc
|
||||
|
||||
# with short syntax
|
||||
hgctl dash envoy
|
||||
hgctl d envoy
|
||||
`,
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
kubeClient, err := kubernetes.NewCLIClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
|
||||
if err != nil {
|
||||
return fmt.Errorf("build CLI client fail: %w", err)
|
||||
}
|
||||
|
||||
if labelSelector == "" && len(args) < 1 {
|
||||
c.Println(c.UsageString())
|
||||
return fmt.Errorf("specify a pod or --selector")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create k8s client: %v", err)
|
||||
}
|
||||
|
||||
var podName, ns string
|
||||
if labelSelector != "" {
|
||||
pl, err := kubeClient.PodsForSelector(envoyDashNs, labelSelector)
|
||||
if err != nil {
|
||||
return fmt.Errorf("not able to locate pod with selector %s: %v", labelSelector, err)
|
||||
}
|
||||
|
||||
if len(pl.Items) < 1 {
|
||||
return errors.New("no pods found")
|
||||
}
|
||||
// only use the first pod in the list
|
||||
podName = pl.Items[0].Name
|
||||
ns = pl.Items[0].Namespace
|
||||
} else if len(args) > 0 {
|
||||
po, err := kubeClient.Pod(types.NamespacedName{Name: args[0], Namespace: envoyDashNs})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
podName = po.Name
|
||||
ns = po.Namespace
|
||||
}
|
||||
|
||||
return portForward(podName, ns, fmt.Sprintf("Envoy sidecar %s", podName),
|
||||
"http://%s", bindAddress, proxyAdminPort, kubeClient, c.OutOrStdout(), browser)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// portForward first tries to forward localhost:remotePort to podName:remotePort, falls back to dynamic local port
|
||||
func portForward(podName, namespace, flavor, urlFormat, localAddress string, remotePort int,
|
||||
client kubernetes.CLIClient, writer io.Writer, browser bool,
|
||||
) error {
|
||||
// port preference:
|
||||
// - If --listenPort is specified, use it
|
||||
// - without --listenPort, prefer the remotePort but fall back to a random port
|
||||
var portPrefs []int
|
||||
if listenPort != 0 {
|
||||
portPrefs = []int{listenPort}
|
||||
} else {
|
||||
portPrefs = []int{remotePort}
|
||||
}
|
||||
|
||||
var err error
|
||||
for _, localPort := range portPrefs {
|
||||
var fw kubernetes.PortForwarder
|
||||
fw, err = kubernetes.NewLocalPortForwarder(client, types.NamespacedName{Namespace: namespace, Name: podName}, localPort, remotePort)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not build port forwarder for %s: %v", flavor, err)
|
||||
}
|
||||
|
||||
if err := fw.Start(); err != nil {
|
||||
fw.Stop()
|
||||
// Try the next port
|
||||
continue
|
||||
}
|
||||
|
||||
// Close the port forwarder when the command is terminated.
|
||||
ClosePortForwarderOnInterrupt(fw)
|
||||
|
||||
openBrowser(fmt.Sprintf(urlFormat, fw.Address()), writer, browser)
|
||||
|
||||
// Wait for stop
|
||||
fw.WaitForStop()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failure running port forward process: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ClosePortForwarderOnInterrupt(fw kubernetes.PortForwarder) {
|
||||
go func() {
|
||||
signals := make(chan os.Signal, 1)
|
||||
signal.Notify(signals, os.Interrupt)
|
||||
defer signal.Stop(signals)
|
||||
<-signals
|
||||
fw.Stop()
|
||||
}()
|
||||
}
|
||||
|
||||
func openBrowser(url string, writer io.Writer, browser bool) {
|
||||
var err error
|
||||
|
||||
fmt.Fprintf(writer, "%s\n", url)
|
||||
|
||||
if !browser {
|
||||
fmt.Fprint(writer, "skipping opening a browser")
|
||||
return
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
err = exec.Command("xdg-open", url).Start()
|
||||
case "windows":
|
||||
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
|
||||
case "darwin":
|
||||
err = exec.Command("open", url).Start()
|
||||
default:
|
||||
fmt.Fprintf(writer, "Unsupported platform %q; open %s in your browser.\n", runtime.GOOS, url)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(writer, "Failed to open browser; open %s in your browser.\nError: %s\n", url, err.Error())
|
||||
}
|
||||
}
|
||||
297
pkg/cmd/hgctl/helm/common.go
Normal file
297
pkg/cmd/hgctl/helm/common.go
Normal file
@@ -0,0 +1,297 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package helm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/helm/tpath"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/util"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
profileName := DefaultProfileName
|
||||
// The profile coming from --set flag has the highest precedence.
|
||||
psf := GetValueForSetFlag(setFlags, "profile")
|
||||
if psf != "" {
|
||||
profileName = psf
|
||||
}
|
||||
return "", profileName, nil
|
||||
}
|
||||
|
||||
func GetUninstallProfileName() string {
|
||||
return DefaultUninstallProfileName
|
||||
}
|
||||
|
||||
func ReadLayeredYAMLs(filenames []string) (string, error) {
|
||||
return readLayeredYAMLs(filenames, os.Stdin)
|
||||
}
|
||||
|
||||
func readLayeredYAMLs(filenames []string, stdinReader io.Reader) (string, error) {
|
||||
var ly string
|
||||
var stdin bool
|
||||
for _, fn := range filenames {
|
||||
var b []byte
|
||||
var err error
|
||||
if fn == "-" {
|
||||
if stdin {
|
||||
continue
|
||||
}
|
||||
stdin = true
|
||||
b, err = io.ReadAll(stdinReader)
|
||||
} else {
|
||||
b, err = os.ReadFile(strings.TrimSpace(fn))
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ly, err = util.OverlayYAML(ly, string(b))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return ly, nil
|
||||
}
|
||||
|
||||
// GetValueForSetFlag parses the passed set flags which have format key=value and if any set the given path,
|
||||
// returns the corresponding value, otherwise returns the empty string. setFlags must have valid format.
|
||||
func GetValueForSetFlag(setFlags []string, path string) string {
|
||||
ret := ""
|
||||
for _, sf := range setFlags {
|
||||
p, v := getPV(sf)
|
||||
if p == path {
|
||||
ret = v
|
||||
}
|
||||
// if set multiple times, return last set value
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// getPV returns the path and value components for the given set flag string, which must be in path=value format.
|
||||
func getPV(setFlag string) (path string, value string) {
|
||||
pv := strings.Split(setFlag, "=")
|
||||
if len(pv) != 2 {
|
||||
return setFlag, ""
|
||||
}
|
||||
path, value = strings.TrimSpace(pv[0]), strings.TrimSpace(pv[1])
|
||||
return
|
||||
}
|
||||
|
||||
func GenerateConfig(inFilenames []string, setFlags []string) (string, *Profile, string, error) {
|
||||
if err := validateSetFlags(setFlags); err != nil {
|
||||
return "", nil, "", err
|
||||
}
|
||||
|
||||
fy, profileName, err := ReadYamlProfile(inFilenames, setFlags)
|
||||
if err != nil {
|
||||
return "", nil, "", err
|
||||
}
|
||||
|
||||
profileString, profile, err := GenProfile(profileName, fy, setFlags)
|
||||
|
||||
if err != nil {
|
||||
return "", nil, "", err
|
||||
}
|
||||
|
||||
return profileString, profile, profileName, nil
|
||||
}
|
||||
|
||||
// validateSetFlags validates that setFlags all have path=value format.
|
||||
func validateSetFlags(setFlags []string) error {
|
||||
for _, sf := range setFlags {
|
||||
pv := strings.Split(sf, "=")
|
||||
if len(pv) != 2 {
|
||||
return fmt.Errorf("set flag %s has incorrect format, must be path=value", sf)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func overlaySetFlagValues(iopYAML string, setFlags []string) (string, error) {
|
||||
iop := make(map[string]any)
|
||||
if err := yaml.Unmarshal([]byte(iopYAML), &iop); err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Unmarshal returns nil for empty manifests but we need something to insert into.
|
||||
if iop == nil {
|
||||
iop = make(map[string]any)
|
||||
}
|
||||
|
||||
for _, sf := range setFlags {
|
||||
p, v := getPV(sf)
|
||||
inc, _, err := tpath.GetPathContext(iop, util.PathFromString(p), true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// input value type is always string, transform it to correct type before setting.
|
||||
if err := tpath.WritePathContext(inc, util.ParseValue(v), false); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
out, err := yaml.Marshal(iop)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
// getInstallPackagePath returns the installPackagePath in the given IstioOperator YAML string.
|
||||
func getInstallPackagePath(profileYAML string) (string, error) {
|
||||
profile, err := UnmarshalProfile(profileYAML)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if profile == nil {
|
||||
return "", nil
|
||||
}
|
||||
return profile.InstallPackagePath, nil
|
||||
}
|
||||
|
||||
// GetProfileYAML returns the YAML for the given profile name, using the given profileOrPath string, which may be either
|
||||
// a profile label or a file path.
|
||||
func GetProfileYAML(installPackagePath, profileOrPath string) (string, error) {
|
||||
if profileOrPath == "" {
|
||||
profileOrPath = DefaultProfileFilename
|
||||
}
|
||||
profiles, err := readProfiles(installPackagePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read profiles: %v", err)
|
||||
}
|
||||
// If charts are a file path and profile is a name like default, transform it to the file path.
|
||||
if profiles[profileOrPath] && installPackagePath != "" {
|
||||
profileOrPath = filepath.Join(installPackagePath, "profiles", profileOrPath+".yaml")
|
||||
}
|
||||
// This contains the IstioOperator CR.
|
||||
baseCRYAML, err := ReadProfileYAML(profileOrPath, installPackagePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
//if !IsDefaultProfile(profileOrPath) {
|
||||
// // Profile definitions are relative to the default profileOrPath, so read that first.
|
||||
// dfn := DefaultFilenameForProfile(profileOrPath)
|
||||
// defaultYAML, err := ReadProfileYAML(dfn, installPackagePath)
|
||||
// if err != nil {
|
||||
// return "", err
|
||||
// }
|
||||
// baseCRYAML, err = util.OverlayYAML(defaultYAML, baseCRYAML)
|
||||
// if err != nil {
|
||||
// return "", err
|
||||
// }
|
||||
//}
|
||||
return baseCRYAML, nil
|
||||
}
|
||||
|
||||
// IsDefaultProfile reports whether the given profile is the default profile.
|
||||
func IsDefaultProfile(profile string) bool {
|
||||
return profile == "" || profile == DefaultProfileName || filepath.Base(profile) == DefaultProfileFilename
|
||||
}
|
||||
|
||||
// DefaultFilenameForProfile returns the profile name of the default profile for the given profile.
|
||||
func DefaultFilenameForProfile(profile string) string {
|
||||
switch {
|
||||
case util.IsFilePath(profile):
|
||||
return filepath.Join(filepath.Dir(profile), DefaultProfileFilename)
|
||||
default:
|
||||
return DefaultProfileName
|
||||
}
|
||||
}
|
||||
|
||||
// ReadProfileYAML reads the YAML values associated with the given profile. It uses an appropriate reader for the
|
||||
// profile format (compiled-in, file, HTTP, etc.).
|
||||
func ReadProfileYAML(profile, manifestsPath string) (string, error) {
|
||||
var err error
|
||||
var globalValues string
|
||||
|
||||
// Get global values from profile.
|
||||
switch {
|
||||
case util.IsFilePath(profile):
|
||||
if globalValues, err = readFile(profile); err != nil {
|
||||
return "", err
|
||||
}
|
||||
default:
|
||||
if globalValues, err = LoadValues(profile, manifestsPath); err != nil {
|
||||
return "", fmt.Errorf("failed to read profile %v from %v: %v", profile, manifestsPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return globalValues, nil
|
||||
}
|
||||
|
||||
func readFile(path string) (string, error) {
|
||||
b, err := os.ReadFile(path)
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
// UnmarshalProfile unmarshals a string containing Profile as YAML.
|
||||
func UnmarshalProfile(profileYAML string) (*Profile, error) {
|
||||
profile := &Profile{}
|
||||
if err := yaml.Unmarshal([]byte(profileYAML), profile); err != nil {
|
||||
return nil, fmt.Errorf("%s:\n\nYAML:\n%s", err, profileYAML)
|
||||
}
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
// GenProfile generates an Profile from the given profile name or path, and overlay YAMLs from user
|
||||
// files and the --set flag. If successful, it returns an Profile string and struct.
|
||||
func GenProfile(profileOrPath, fileOverlayYAML string, setFlags []string) (string, *Profile, error) {
|
||||
installPackagePath, err := getInstallPackagePath(fileOverlayYAML)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if sfp := GetValueForSetFlag(setFlags, "installPackagePath"); sfp != "" {
|
||||
// set flag installPackagePath has the highest precedence, if set.
|
||||
installPackagePath = sfp
|
||||
}
|
||||
|
||||
// To generate the base profileOrPath for overlaying with user values, we need the installPackagePath where the profiles
|
||||
// can be found, and the selected profileOrPath. Both of these can come from either the user overlay file or --set flag.
|
||||
outYAML, err := GetProfileYAML(installPackagePath, profileOrPath)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// Combine file and --set overlays and translate any K8s settings in values to Profile format
|
||||
overlayYAML, err := overlaySetFlagValues(fileOverlayYAML, setFlags)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
// Merge user file and --set flags.
|
||||
outYAML, err = util.OverlayYAML(outYAML, overlayYAML)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("could not overlay user config over base: %s", err)
|
||||
}
|
||||
|
||||
finalProfile, err := UnmarshalProfile(outYAML)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
finalProfile.InstallPackagePath = installPackagePath
|
||||
|
||||
if finalProfile.Profile == "" {
|
||||
finalProfile.Profile = DefaultProfileName
|
||||
}
|
||||
return util.ToYAML(finalProfile), finalProfile, nil
|
||||
}
|
||||
63
pkg/cmd/hgctl/helm/name/name.go
Normal file
63
pkg/cmd/hgctl/helm/name/name.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package name
|
||||
|
||||
// Kubernetes Kind strings.
|
||||
const (
|
||||
CRDStr = "CustomResourceDefinition"
|
||||
ClusterRoleStr = "ClusterRole"
|
||||
ClusterRoleBindingStr = "ClusterRoleBinding"
|
||||
CMStr = "ConfigMap"
|
||||
DaemonSetStr = "DaemonSet"
|
||||
DeploymentStr = "Deployment"
|
||||
EndpointStr = "Endpoints"
|
||||
HPAStr = "HorizontalPodAutoscaler"
|
||||
IngressStr = "Ingress"
|
||||
IstioOperator = "IstioOperator"
|
||||
MutatingWebhookConfigurationStr = "MutatingWebhookConfiguration"
|
||||
NamespaceStr = "Namespace"
|
||||
PVCStr = "PersistentVolumeClaim"
|
||||
PodStr = "Pod"
|
||||
PDBStr = "PodDisruptionBudget"
|
||||
ReplicationControllerStr = "ReplicationController"
|
||||
ReplicaSetStr = "ReplicaSet"
|
||||
RoleStr = "Role"
|
||||
RoleBindingStr = "RoleBinding"
|
||||
SAStr = "ServiceAccount"
|
||||
ServiceStr = "Service"
|
||||
SecretStr = "Secret"
|
||||
StatefulSetStr = "StatefulSet"
|
||||
ValidatingWebhookConfigurationStr = "ValidatingWebhookConfiguration"
|
||||
)
|
||||
|
||||
// Istio Kind strings
|
||||
const (
|
||||
EnvoyFilterStr = "EnvoyFilter"
|
||||
GatewayStr = "Gateway"
|
||||
DestinationRuleStr = "DestinationRule"
|
||||
MeshPolicyStr = "MeshPolicy"
|
||||
PeerAuthenticationStr = "PeerAuthentication"
|
||||
VirtualServiceStr = "VirtualService"
|
||||
IstioOperatorStr = "IstioOperator"
|
||||
)
|
||||
|
||||
// Istio API Group Names
|
||||
const (
|
||||
AuthenticationAPIGroupName = "authentication.istio.io"
|
||||
ConfigAPIGroupName = "config.istio.io"
|
||||
NetworkingAPIGroupName = "networking.istio.io"
|
||||
OperatorAPIGroupName = "operator.istio.io"
|
||||
SecurityAPIGroupName = "security.istio.io"
|
||||
)
|
||||
573
pkg/cmd/hgctl/helm/object/objects.go
Normal file
573
pkg/cmd/hgctl/helm/object/objects.go
Normal file
@@ -0,0 +1,573 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
names "github.com/alibaba/higress/pkg/cmd/hgctl/helm/name"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/helm/tpath"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/util"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
const (
|
||||
// YAMLSeparator is a separator for multi-document YAML files.
|
||||
YAMLSeparator = "\n---\n"
|
||||
)
|
||||
|
||||
// K8sObject is an in-memory representation of a k8s object, used for moving between different representations
|
||||
// (Unstructured, JSON, YAML) with cached rendering.
|
||||
type K8sObject struct {
|
||||
object *unstructured.Unstructured
|
||||
|
||||
Group string
|
||||
Kind string
|
||||
Name string
|
||||
Namespace string
|
||||
|
||||
json []byte
|
||||
yaml []byte
|
||||
}
|
||||
|
||||
// NewK8sObject creates a new K8sObject and returns a ptr to it.
|
||||
func NewK8sObject(u *unstructured.Unstructured, json, yaml []byte) *K8sObject {
|
||||
o := &K8sObject{
|
||||
object: u,
|
||||
json: json,
|
||||
yaml: yaml,
|
||||
}
|
||||
|
||||
gvk := u.GetObjectKind().GroupVersionKind()
|
||||
o.Group = gvk.Group
|
||||
o.Kind = gvk.Kind
|
||||
o.Name = u.GetName()
|
||||
o.Namespace = u.GetNamespace()
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
// Hash returns a unique, insecure hash based on kind, namespace and name.
|
||||
func Hash(kind, namespace, name string) string {
|
||||
switch kind {
|
||||
case names.ClusterRoleStr, names.ClusterRoleBindingStr, names.MeshPolicyStr:
|
||||
namespace = ""
|
||||
}
|
||||
return strings.Join([]string{kind, namespace, name}, ":")
|
||||
}
|
||||
|
||||
// FromHash parses kind, namespace and name from a hash.
|
||||
func FromHash(hash string) (kind, namespace, name string) {
|
||||
hv := strings.Split(hash, ":")
|
||||
if len(hv) != 3 {
|
||||
return "Bad hash string: " + hash, "", ""
|
||||
}
|
||||
kind, namespace, name = hv[0], hv[1], hv[2]
|
||||
return
|
||||
}
|
||||
|
||||
// HashNameKind returns a unique, insecure hash based on kind and name.
|
||||
func HashNameKind(kind, name string) string {
|
||||
return strings.Join([]string{kind, name}, ":")
|
||||
}
|
||||
|
||||
// ParseJSONToK8sObject parses JSON to an K8sObject.
|
||||
func ParseJSONToK8sObject(json []byte) (*K8sObject, error) {
|
||||
o, _, err := unstructured.UnstructuredJSONScheme.Decode(json, nil, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing json into unstructured object: %v", err)
|
||||
}
|
||||
|
||||
u, ok := o.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("parsed unexpected type %T", o)
|
||||
}
|
||||
|
||||
return NewK8sObject(u, json, nil), nil
|
||||
}
|
||||
|
||||
// ParseYAMLToK8sObject parses YAML to an Object.
|
||||
func ParseYAMLToK8sObject(yaml []byte) (*K8sObject, error) {
|
||||
r := bytes.NewReader(yaml)
|
||||
decoder := k8syaml.NewYAMLOrJSONDecoder(r, 1024)
|
||||
|
||||
out := &unstructured.Unstructured{}
|
||||
err := decoder.Decode(out)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding object %v: %v", string(yaml), err)
|
||||
}
|
||||
return NewK8sObject(out, nil, yaml), nil
|
||||
}
|
||||
|
||||
// UnstructuredObject exposes the raw object, primarily for testing
|
||||
func (o *K8sObject) UnstructuredObject() *unstructured.Unstructured {
|
||||
return o.object
|
||||
}
|
||||
|
||||
// ResolveK8sConflict - This method resolves k8s object possible
|
||||
// conflicting settings. Which K8sObjects may need such method
|
||||
// depends on the type of the K8sObject.
|
||||
func (o *K8sObject) ResolveK8sConflict() *K8sObject {
|
||||
if o.Kind == names.PDBStr {
|
||||
return resolvePDBConflict(o)
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
// Unstructured exposes the raw object content, primarily for testing
|
||||
func (o *K8sObject) Unstructured() map[string]any {
|
||||
return o.UnstructuredObject().UnstructuredContent()
|
||||
}
|
||||
|
||||
// Container returns a container subtree for Deployment objects if one is found, or nil otherwise.
|
||||
func (o *K8sObject) Container(name string) map[string]any {
|
||||
u := o.Unstructured()
|
||||
path := fmt.Sprintf("spec.template.spec.containers.[name:%s]", name)
|
||||
node, f, err := tpath.GetPathContext(u, util.PathFromString(path), false)
|
||||
if err == nil && f {
|
||||
// Must be the type from the schema.
|
||||
return node.Node.(map[string]any)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GroupVersionKind returns the GroupVersionKind for the K8sObject
|
||||
func (o *K8sObject) GroupVersionKind() schema.GroupVersionKind {
|
||||
return o.object.GroupVersionKind()
|
||||
}
|
||||
|
||||
// Version returns the APIVersion of the K8sObject
|
||||
func (o *K8sObject) Version() string {
|
||||
return o.object.GetAPIVersion()
|
||||
}
|
||||
|
||||
// Hash returns a unique hash for the K8sObject
|
||||
func (o *K8sObject) Hash() string {
|
||||
return Hash(o.Kind, o.Namespace, o.Name)
|
||||
}
|
||||
|
||||
// HashNameKind returns a hash for the K8sObject based on the name and kind only.
|
||||
func (o *K8sObject) HashNameKind() string {
|
||||
return HashNameKind(o.Kind, o.Name)
|
||||
}
|
||||
|
||||
// JSON returns a JSON representation of the K8sObject, using an internal cache.
|
||||
func (o *K8sObject) JSON() ([]byte, error) {
|
||||
if o.json != nil {
|
||||
return o.json, nil
|
||||
}
|
||||
|
||||
b, err := o.object.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// YAML returns a YAML representation of the K8sObject, using an internal cache.
|
||||
func (o *K8sObject) YAML() ([]byte, error) {
|
||||
if o == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if o.yaml != nil {
|
||||
return o.yaml, nil
|
||||
}
|
||||
oj, err := o.JSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o.json = oj
|
||||
y, err := yaml.JSONToYAML(oj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o.yaml = y
|
||||
return y, nil
|
||||
}
|
||||
|
||||
// YAMLDebugString returns a YAML representation of the K8sObject, or an error string if the K8sObject cannot be rendered to YAML.
|
||||
func (o *K8sObject) YAMLDebugString() string {
|
||||
y, err := o.YAML()
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return string(y)
|
||||
}
|
||||
|
||||
// K8sObjects holds a collection of k8s objects, so that we can filter / sequence them
|
||||
type K8sObjects []*K8sObject
|
||||
|
||||
// String implements the Stringer interface.
|
||||
func (os K8sObjects) String() string {
|
||||
var out []string
|
||||
for _, oo := range os {
|
||||
out = append(out, oo.YAMLDebugString())
|
||||
}
|
||||
return strings.Join(out, YAMLSeparator)
|
||||
}
|
||||
|
||||
// Keys returns a slice with the keys of os.
|
||||
func (os K8sObjects) Keys() []string {
|
||||
var out []string
|
||||
for _, oo := range os {
|
||||
out = append(out, oo.Hash())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// UnstructuredItems returns the list of items of unstructured.Unstructured.
|
||||
func (os K8sObjects) UnstructuredItems() []unstructured.Unstructured {
|
||||
var usList []unstructured.Unstructured
|
||||
for _, obj := range os {
|
||||
usList = append(usList, *obj.UnstructuredObject())
|
||||
}
|
||||
return usList
|
||||
}
|
||||
|
||||
// ParseK8sObjectsFromYAMLManifest returns a K8sObjects representation of manifest.
|
||||
func ParseK8sObjectsFromYAMLManifest(manifest string) (K8sObjects, error) {
|
||||
return ParseK8sObjectsFromYAMLManifestFailOption(manifest, true)
|
||||
}
|
||||
|
||||
// ParseK8sObjectsFromYAMLManifestFailOption returns a K8sObjects representation of manifest. Continues parsing when a bad object
|
||||
// is found if failOnError is set to false.
|
||||
func ParseK8sObjectsFromYAMLManifestFailOption(manifest string, failOnError bool) (K8sObjects, error) {
|
||||
var b bytes.Buffer
|
||||
|
||||
var yamls []string
|
||||
scanner := bufio.NewScanner(strings.NewReader(manifest))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "---") {
|
||||
// yaml separator
|
||||
yamls = append(yamls, b.String())
|
||||
b.Reset()
|
||||
} else {
|
||||
if _, err := b.WriteString(line); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := b.WriteString("\n"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
yamls = append(yamls, b.String())
|
||||
|
||||
var objects K8sObjects
|
||||
|
||||
for _, yaml := range yamls {
|
||||
yaml = removeNonYAMLLines(yaml)
|
||||
if yaml == "" {
|
||||
continue
|
||||
}
|
||||
o, err := ParseYAMLToK8sObject([]byte(yaml))
|
||||
if err != nil {
|
||||
e := fmt.Errorf("failed to parse YAML to a k8s object: %s", err)
|
||||
if failOnError {
|
||||
return nil, e
|
||||
}
|
||||
continue
|
||||
}
|
||||
if o.Valid() {
|
||||
objects = append(objects, o)
|
||||
}
|
||||
}
|
||||
|
||||
return objects, nil
|
||||
}
|
||||
|
||||
func removeNonYAMLLines(yms string) string {
|
||||
var b strings.Builder
|
||||
for _, s := range strings.Split(yms, "\n") {
|
||||
if strings.HasPrefix(s, "#") {
|
||||
continue
|
||||
}
|
||||
b.WriteString(s)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
// helm charts sometimes emits blank objects with just a "disabled" comment.
|
||||
return strings.TrimSpace(b.String())
|
||||
}
|
||||
|
||||
// YAMLManifest returns a YAML representation of K8sObjects os.
|
||||
func (os K8sObjects) YAMLManifest() (string, error) {
|
||||
var b bytes.Buffer
|
||||
|
||||
for i, item := range os {
|
||||
if i != 0 {
|
||||
if _, err := b.WriteString("\n\n"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
ym, err := item.YAML()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error building yaml: %v", err)
|
||||
}
|
||||
if _, err := b.Write(ym); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := b.Write([]byte(YAMLSeparator)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
// Sort will order the items in K8sObjects in order of score, group, kind, name. The intent is to
|
||||
// have a deterministic ordering in which K8sObjects are applied.
|
||||
func (os K8sObjects) Sort(score func(o *K8sObject) int) {
|
||||
sort.Slice(os, func(i, j int) bool {
|
||||
iScore := score(os[i])
|
||||
jScore := score(os[j])
|
||||
return iScore < jScore ||
|
||||
(iScore == jScore &&
|
||||
os[i].Group < os[j].Group) ||
|
||||
(iScore == jScore &&
|
||||
os[i].Group == os[j].Group &&
|
||||
os[i].Kind < os[j].Kind) ||
|
||||
(iScore == jScore &&
|
||||
os[i].Group == os[j].Group &&
|
||||
os[i].Kind == os[j].Kind &&
|
||||
os[i].Name < os[j].Name)
|
||||
})
|
||||
}
|
||||
|
||||
// ToMap returns a map of K8sObject hash to K8sObject.
|
||||
func (os K8sObjects) ToMap() map[string]*K8sObject {
|
||||
ret := make(map[string]*K8sObject)
|
||||
for _, oo := range os {
|
||||
if oo.Valid() {
|
||||
ret[oo.Hash()] = oo
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// ToNameKindMap returns a map of K8sObject name/kind hash to K8sObject.
|
||||
func (os K8sObjects) ToNameKindMap() map[string]*K8sObject {
|
||||
ret := make(map[string]*K8sObject)
|
||||
for _, oo := range os {
|
||||
if oo.Valid() {
|
||||
ret[oo.HashNameKind()] = oo
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Valid checks returns true if Kind of K8sObject is not empty.
|
||||
func (o *K8sObject) Valid() bool {
|
||||
return o.Kind != ""
|
||||
}
|
||||
|
||||
// FullName returns namespace/name of K8s object
|
||||
func (o *K8sObject) FullName() string {
|
||||
return fmt.Sprintf("%s/%s", o.Namespace, o.Name)
|
||||
}
|
||||
|
||||
// Equal returns true if o and other are both valid and equal to each other.
|
||||
func (o *K8sObject) Equal(other *K8sObject) bool {
|
||||
if o == nil {
|
||||
return other == nil
|
||||
}
|
||||
if other == nil {
|
||||
return o == nil
|
||||
}
|
||||
|
||||
ay, err := o.YAML()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
by, err := other.YAML()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return util.IsYAMLEqual(string(ay), string(by))
|
||||
}
|
||||
|
||||
func istioCustomResources(group string) bool {
|
||||
switch group {
|
||||
case names.ConfigAPIGroupName,
|
||||
names.SecurityAPIGroupName,
|
||||
names.AuthenticationAPIGroupName,
|
||||
names.NetworkingAPIGroupName:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DefaultObjectOrder is default sorting function used to sort k8s objects.
|
||||
func DefaultObjectOrder() func(o *K8sObject) int {
|
||||
return func(o *K8sObject) int {
|
||||
gk := o.Group + "/" + o.Kind
|
||||
switch {
|
||||
// Create CRDs asap - both because they are slow and because we will likely create instances of them soon
|
||||
case gk == "apiextensions.k8s.io/CustomResourceDefinition":
|
||||
return -1000
|
||||
|
||||
// We need to create ServiceAccounts, Roles before we bind them with a RoleBinding
|
||||
case gk == "/ServiceAccount" || gk == "rbac.authorization.k8s.io/ClusterRole":
|
||||
return 1
|
||||
case gk == "rbac.authorization.k8s.io/ClusterRoleBinding":
|
||||
return 2
|
||||
|
||||
// validatingwebhookconfiguration is configured to FAIL-OPEN in the default install. For the
|
||||
// re-install case we want to apply the validatingwebhookconfiguration first to reset any
|
||||
// orphaned validatingwebhookconfiguration that is FAIL-CLOSE.
|
||||
case gk == "admissionregistration.k8s.io/ValidatingWebhookConfiguration":
|
||||
return 3
|
||||
|
||||
case istioCustomResources(o.Group):
|
||||
return 4
|
||||
|
||||
// Pods might need configmap or secrets - avoid backoff by creating them first
|
||||
case gk == "/ConfigMap" || gk == "/Secrets":
|
||||
return 100
|
||||
|
||||
// Create the pods after we've created other things they might be waiting for
|
||||
case gk == "extensions/Deployment" || gk == "app/Deployment":
|
||||
return 1000
|
||||
|
||||
// Autoscalers typically act on a deployment
|
||||
case gk == "autoscaling/HorizontalPodAutoscaler":
|
||||
return 1001
|
||||
|
||||
// Create services late - after pods have been started
|
||||
case gk == "/Service":
|
||||
return 10000
|
||||
|
||||
default:
|
||||
return 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ObjectsNotInLists(objects K8sObjects, lists ...K8sObjects) K8sObjects {
|
||||
var ret K8sObjects
|
||||
|
||||
filterMap := make(map[*K8sObject]bool)
|
||||
for _, list := range lists {
|
||||
for _, object := range list {
|
||||
filterMap[object] = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, o := range objects {
|
||||
if !filterMap[o] {
|
||||
ret = append(ret, o)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// KindObjects returns the subset of objs with the given kind.
|
||||
func KindObjects(objs K8sObjects, kind string) K8sObjects {
|
||||
var ret K8sObjects
|
||||
for _, o := range objs {
|
||||
if o.Kind == kind {
|
||||
ret = append(ret, o)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
//// ParseK8SYAMLToIstioOperator parses a IstioOperator CustomResource YAML string and unmarshals in into
|
||||
//// an IstioOperatorSpec object. It returns the object and an API group/version with it.
|
||||
//func ParseK8SYAMLToIstioOperator(yml string) (*v1alpha1.HigressOperator, *schema.GroupVersionKind, error) {
|
||||
// o, err := ParseYAMLToK8sObject([]byte(yml))
|
||||
// if err != nil {
|
||||
// return nil, nil, err
|
||||
// }
|
||||
// iop := &v1alpha1.HigressOperator{}
|
||||
// if err := yaml.UnmarshalStrict([]byte(yml), iop); err != nil {
|
||||
// return nil, nil, err
|
||||
// }
|
||||
// gvk := o.GroupVersionKind()
|
||||
// //v1alpha1.SetNamespace(iop.Spec, o.Namespace)
|
||||
// return iop, &gvk, nil
|
||||
//}
|
||||
|
||||
// AllObjectHashes returns a map with object hashes of all the objects contained in cmm as the keys.
|
||||
func AllObjectHashes(m string) map[string]bool {
|
||||
ret := make(map[string]bool)
|
||||
objs, err := ParseK8sObjectsFromYAMLManifest(m)
|
||||
if err != nil {
|
||||
}
|
||||
for _, o := range objs {
|
||||
ret[o.Hash()] = true
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// resolvePDBConflict When user uses both minAvailable and
|
||||
// maxUnavailable to configure istio instances, these two
|
||||
// parameters are mutually exclusive, care must be taken
|
||||
// to resolve the issue
|
||||
func resolvePDBConflict(o *K8sObject) *K8sObject {
|
||||
if o.json == nil {
|
||||
return o
|
||||
}
|
||||
if o.object.Object["spec"] == nil {
|
||||
return o
|
||||
}
|
||||
spec := o.object.Object["spec"].(map[string]any)
|
||||
isDefault := func(item any) bool {
|
||||
var ii intstr.IntOrString
|
||||
switch item := item.(type) {
|
||||
case int:
|
||||
ii = intstr.FromInt(item)
|
||||
case int64:
|
||||
ii = intstr.FromInt(int(item))
|
||||
case string:
|
||||
ii = intstr.FromString(item)
|
||||
default:
|
||||
ii = intstr.FromInt(0)
|
||||
}
|
||||
intVal, err := intstr.GetScaledValueFromIntOrPercent(&ii, 100, false)
|
||||
if err != nil || intVal == 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
if spec["maxUnavailable"] != nil && spec["minAvailable"] != nil {
|
||||
// When both maxUnavailable and minAvailable present and
|
||||
// neither has value 0, this is considered a conflict,
|
||||
// then maxUnavailale will take precedence.
|
||||
if !isDefault(spec["maxUnavailable"]) && !isDefault(spec["minAvailable"]) {
|
||||
delete(spec, "minAvailable")
|
||||
// Make sure that the json and yaml representation of the object
|
||||
// is consistent with the changed object
|
||||
o.json = nil
|
||||
o.json, _ = o.JSON()
|
||||
if o.yaml != nil {
|
||||
o.yaml = nil
|
||||
o.yaml, _ = o.YAML()
|
||||
}
|
||||
}
|
||||
}
|
||||
return o
|
||||
}
|
||||
713
pkg/cmd/hgctl/helm/object/objects_test.go
Normal file
713
pkg/cmd/hgctl/helm/object/objects_test.go
Normal file
@@ -0,0 +1,713 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package object
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/util"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
func TestHash(t *testing.T) {
|
||||
hashTests := []struct {
|
||||
desc string
|
||||
kind string
|
||||
namespace string
|
||||
name string
|
||||
want string
|
||||
}{
|
||||
{"CalculateHashForObjectWithNormalCharacter", "Service", "default", "ingressgateway", "Service:default:ingressgateway"},
|
||||
{"CalculateHashForObjectWithDash", "Deployment", "istio-system", "istio-pilot", "Deployment:istio-system:istio-pilot"},
|
||||
{"CalculateHashForObjectWithDot", "ConfigMap", "istio-system", "my.config", "ConfigMap:istio-system:my.config"},
|
||||
}
|
||||
|
||||
for _, tt := range hashTests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
got := Hash(tt.kind, tt.namespace, tt.name)
|
||||
if got != tt.want {
|
||||
t.Errorf("Hash(%s): got %s for kind %s, namespace %s, name %s, want %s", tt.desc, got, tt.kind, tt.namespace, tt.name, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromHash(t *testing.T) {
|
||||
hashTests := []struct {
|
||||
desc string
|
||||
hash string
|
||||
kind string
|
||||
namespace string
|
||||
name string
|
||||
}{
|
||||
{"ParseHashWithNormalCharacter", "Service:default:ingressgateway", "Service", "default", "ingressgateway"},
|
||||
{"ParseHashForObjectWithDash", "Deployment:istio-system:istio-pilot", "Deployment", "istio-system", "istio-pilot"},
|
||||
{"ParseHashForObjectWithDot", "ConfigMap:istio-system:my.config", "ConfigMap", "istio-system", "my.config"},
|
||||
{"InvalidHash", "test", "Bad hash string: test", "", ""},
|
||||
}
|
||||
|
||||
for _, tt := range hashTests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
k, ns, name := FromHash(tt.hash)
|
||||
if k != tt.kind || ns != tt.namespace || name != tt.name {
|
||||
t.Errorf("FromHash(%s): got kind %s, namespace %s, name %s, want kind %s, namespace %s, name %s", tt.desc, k, ns, name, tt.kind, tt.namespace, tt.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashNameKind(t *testing.T) {
|
||||
hashNameKindTests := []struct {
|
||||
desc string
|
||||
kind string
|
||||
name string
|
||||
want string
|
||||
}{
|
||||
{"CalculateHashNameKindForObjectWithNormalCharacter", "Service", "ingressgateway", "Service:ingressgateway"},
|
||||
{"CalculateHashNameKindForObjectWithDash", "Deployment", "istio-pilot", "Deployment:istio-pilot"},
|
||||
{"CalculateHashNameKindForObjectWithDot", "ConfigMap", "my.config", "ConfigMap:my.config"},
|
||||
}
|
||||
|
||||
for _, tt := range hashNameKindTests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
got := HashNameKind(tt.kind, tt.name)
|
||||
if got != tt.want {
|
||||
t.Errorf("HashNameKind(%s): got %s for kind %s, name %s, want %s", tt.desc, got, tt.kind, tt.name, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseJSONToK8sObject(t *testing.T) {
|
||||
testDeploymentJSON := `{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": {
|
||||
"name": "istio-citadel",
|
||||
"namespace": "istio-system",
|
||||
"labels": {
|
||||
"istio": "citadel"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"replicas": 1,
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"istio": "citadel"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"istio": "citadel"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"name": "citadel",
|
||||
"image": "docker.io/istio/citadel:1.1.8",
|
||||
"args": [
|
||||
"--append-dns-names=true",
|
||||
"--grpc-port=8060",
|
||||
"--grpc-hostname=citadel",
|
||||
"--citadel-storage-namespace=istio-system",
|
||||
"--custom-dns-names=istio-pilot-service-account.istio-system:istio-pilot.istio-system",
|
||||
"--monitoring-port=15014",
|
||||
"--self-signed-ca=true"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
testPodJSON := `{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": {
|
||||
"name": "istio-galley-75bcd59768-hpt5t",
|
||||
"namespace": "istio-system",
|
||||
"labels": {
|
||||
"istio": "galley"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"name": "galley",
|
||||
"image": "docker.io/istio/galley:1.1.8",
|
||||
"command": [
|
||||
"/usr/local/bin/galley",
|
||||
"server",
|
||||
"--meshConfigFile=/etc/mesh-config/mesh",
|
||||
"--livenessProbeInterval=1s",
|
||||
"--livenessProbePath=/healthliveness",
|
||||
"--readinessProbePath=/healthready",
|
||||
"--readinessProbeInterval=1s",
|
||||
"--deployment-namespace=istio-system",
|
||||
"--insecure=true",
|
||||
"--validation-webhook-config-file",
|
||||
"/etc/config/validatingwebhookconfiguration.yaml",
|
||||
"--monitoringPort=15014",
|
||||
"--log_output_level=default:info"
|
||||
],
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 443,
|
||||
"protocol": "TCP"
|
||||
},
|
||||
{
|
||||
"containerPort": 15014,
|
||||
"protocol": "TCP"
|
||||
},
|
||||
{
|
||||
"containerPort": 9901,
|
||||
"protocol": "TCP"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}`
|
||||
testServiceJSON := `{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Service",
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"app": "pilot"
|
||||
},
|
||||
"name": "istio-pilot",
|
||||
"namespace": "istio-system"
|
||||
},
|
||||
"spec": {
|
||||
"clusterIP": "10.102.230.31",
|
||||
"ports": [
|
||||
{
|
||||
"name": "grpc-xds",
|
||||
"port": 15010,
|
||||
"protocol": "TCP",
|
||||
"targetPort": 15010
|
||||
},
|
||||
{
|
||||
"name": "https-xds",
|
||||
"port": 15011,
|
||||
"protocol": "TCP",
|
||||
"targetPort": 15011
|
||||
},
|
||||
{
|
||||
"name": "http-legacy-discovery",
|
||||
"port": 8080,
|
||||
"protocol": "TCP",
|
||||
"targetPort": 8080
|
||||
},
|
||||
{
|
||||
"name": "http-monitoring",
|
||||
"port": 15014,
|
||||
"protocol": "TCP",
|
||||
"targetPort": 15014
|
||||
}
|
||||
],
|
||||
"selector": {
|
||||
"istio": "pilot"
|
||||
},
|
||||
"sessionAffinity": "None",
|
||||
"type": "ClusterIP"
|
||||
}
|
||||
}`
|
||||
|
||||
testInvalidJSON := `invalid json`
|
||||
|
||||
parseJSONToK8sObjectTests := []struct {
|
||||
desc string
|
||||
objString string
|
||||
wantGroup string
|
||||
wantKind string
|
||||
wantName string
|
||||
wantNamespace string
|
||||
wantErr bool
|
||||
}{
|
||||
{"ParseJsonToK8sDeployment", testDeploymentJSON, "apps", "Deployment", "istio-citadel", "istio-system", false},
|
||||
{"ParseJsonToK8sPod", testPodJSON, "", "Pod", "istio-galley-75bcd59768-hpt5t", "istio-system", false},
|
||||
{"ParseJsonToK8sService", testServiceJSON, "", "Service", "istio-pilot", "istio-system", false},
|
||||
{"ParseJsonError", testInvalidJSON, "", "", "", "", true},
|
||||
}
|
||||
|
||||
for _, tt := range parseJSONToK8sObjectTests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
k8sObj, err := ParseJSONToK8sObject([]byte(tt.objString))
|
||||
if err == nil {
|
||||
if tt.wantErr {
|
||||
t.Errorf("ParseJsonToK8sObject(%s): should be error", tt.desc)
|
||||
}
|
||||
k8sObjStr := k8sObj.YAMLDebugString()
|
||||
if k8sObj.Group != tt.wantGroup {
|
||||
t.Errorf("ParseJsonToK8sObject(%s): got group %s for k8s object %s, want %s", tt.desc, k8sObj.Group, k8sObjStr, tt.wantGroup)
|
||||
}
|
||||
if k8sObj.Kind != tt.wantKind {
|
||||
t.Errorf("ParseJsonToK8sObject(%s): got kind %s for k8s object %s, want %s", tt.desc, k8sObj.Kind, k8sObjStr, tt.wantKind)
|
||||
}
|
||||
if k8sObj.Name != tt.wantName {
|
||||
t.Errorf("ParseJsonToK8sObject(%s): got name %s for k8s object %s, want %s", tt.desc, k8sObj.Name, k8sObjStr, tt.wantName)
|
||||
}
|
||||
if k8sObj.Namespace != tt.wantNamespace {
|
||||
t.Errorf("ParseJsonToK8sObject(%s): got group %s for k8s object %s, want %s", tt.desc, k8sObj.Namespace, k8sObjStr, tt.wantNamespace)
|
||||
}
|
||||
} else if !tt.wantErr {
|
||||
t.Errorf("ParseJsonToK8sObject(%s): got unexpected error: %v", tt.desc, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseYAMLToK8sObject(t *testing.T) {
|
||||
testDeploymentYaml := `apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: istio-citadel
|
||||
namespace: istio-system
|
||||
labels:
|
||||
istio: citadel
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
istio: citadel
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
istio: citadel
|
||||
spec:
|
||||
containers:
|
||||
- name: citadel
|
||||
image: docker.io/istio/citadel:1.1.8
|
||||
args:
|
||||
- "--append-dns-names=true"
|
||||
- "--grpc-port=8060"
|
||||
- "--grpc-hostname=citadel"
|
||||
- "--citadel-storage-namespace=istio-system"
|
||||
- "--custom-dns-names=istio-pilot-service-account.istio-system:istio-pilot.istio-system"
|
||||
- "--monitoring-port=15014"
|
||||
- "--self-signed-ca=true"`
|
||||
|
||||
testPodYaml := `apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: istio-galley-75bcd59768-hpt5t
|
||||
namespace: istio-system
|
||||
labels:
|
||||
istio: galley
|
||||
spec:
|
||||
containers:
|
||||
- name: galley
|
||||
image: docker.io/istio/galley:1.1.8
|
||||
command:
|
||||
- "/usr/local/bin/galley"
|
||||
- server
|
||||
- "--meshConfigFile=/etc/mesh-config/mesh"
|
||||
- "--livenessProbeInterval=1s"
|
||||
- "--livenessProbePath=/healthliveness"
|
||||
- "--readinessProbePath=/healthready"
|
||||
- "--readinessProbeInterval=1s"
|
||||
- "--deployment-namespace=istio-system"
|
||||
- "--insecure=true"
|
||||
- "--validation-webhook-config-file"
|
||||
- "/etc/config/validatingwebhookconfiguration.yaml"
|
||||
- "--monitoringPort=15014"
|
||||
- "--log_output_level=default:info"
|
||||
ports:
|
||||
- containerPort: 443
|
||||
protocol: TCP
|
||||
- containerPort: 15014
|
||||
protocol: TCP
|
||||
- containerPort: 9901
|
||||
protocol: TCP`
|
||||
|
||||
testServiceYaml := `apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: pilot
|
||||
name: istio-pilot
|
||||
namespace: istio-system
|
||||
spec:
|
||||
clusterIP: 10.102.230.31
|
||||
ports:
|
||||
- name: grpc-xds
|
||||
port: 15010
|
||||
protocol: TCP
|
||||
targetPort: 15010
|
||||
- name: https-xds
|
||||
port: 15011
|
||||
protocol: TCP
|
||||
targetPort: 15011
|
||||
- name: http-legacy-discovery
|
||||
port: 8080
|
||||
protocol: TCP
|
||||
targetPort: 8080
|
||||
- name: http-monitoring
|
||||
port: 15014
|
||||
protocol: TCP
|
||||
targetPort: 15014
|
||||
selector:
|
||||
istio: pilot
|
||||
sessionAffinity: None
|
||||
type: ClusterIP`
|
||||
|
||||
parseYAMLToK8sObjectTests := []struct {
|
||||
desc string
|
||||
objString string
|
||||
wantGroup string
|
||||
wantKind string
|
||||
wantName string
|
||||
wantNamespace string
|
||||
}{
|
||||
{"ParseYamlToK8sDeployment", testDeploymentYaml, "apps", "Deployment", "istio-citadel", "istio-system"},
|
||||
{"ParseYamlToK8sPod", testPodYaml, "", "Pod", "istio-galley-75bcd59768-hpt5t", "istio-system"},
|
||||
{"ParseYamlToK8sService", testServiceYaml, "", "Service", "istio-pilot", "istio-system"},
|
||||
}
|
||||
|
||||
for _, tt := range parseYAMLToK8sObjectTests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
k8sObj, err := ParseYAMLToK8sObject([]byte(tt.objString))
|
||||
if err != nil {
|
||||
k8sObjStr := k8sObj.YAMLDebugString()
|
||||
if k8sObj.Group != tt.wantGroup {
|
||||
t.Errorf("ParseYAMLToK8sObject(%s): got group %s for k8s object %s, want %s", tt.desc, k8sObj.Group, k8sObjStr, tt.wantGroup)
|
||||
}
|
||||
if k8sObj.Group != tt.wantGroup {
|
||||
t.Errorf("ParseYAMLToK8sObject(%s): got kind %s for k8s object %s, want %s", tt.desc, k8sObj.Kind, k8sObjStr, tt.wantKind)
|
||||
}
|
||||
if k8sObj.Name != tt.wantName {
|
||||
t.Errorf("ParseYAMLToK8sObject(%s): got name %s for k8s object %s, want %s", tt.desc, k8sObj.Name, k8sObjStr, tt.wantName)
|
||||
}
|
||||
if k8sObj.Namespace != tt.wantNamespace {
|
||||
t.Errorf("ParseYAMLToK8sObject(%s): got group %s for k8s object %s, want %s", tt.desc, k8sObj.Namespace, k8sObjStr, tt.wantNamespace)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseK8sObjectsFromYAMLManifest(t *testing.T) {
|
||||
testDeploymentYaml := `apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: istio-citadel
|
||||
namespace: istio-system
|
||||
labels:
|
||||
istio: citadel
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
istio: citadel
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
istio: citadel
|
||||
spec:
|
||||
containers:
|
||||
- name: citadel
|
||||
image: docker.io/istio/citadel:1.1.8
|
||||
args:
|
||||
- "--append-dns-names=true"
|
||||
- "--grpc-port=8060"
|
||||
- "--grpc-hostname=citadel"
|
||||
- "--citadel-storage-namespace=istio-system"
|
||||
- "--custom-dns-names=istio-pilot-service-account.istio-system:istio-pilot.istio-system"
|
||||
- "--monitoring-port=15014"
|
||||
- "--self-signed-ca=true"`
|
||||
|
||||
testPodYaml := `apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: istio-galley-75bcd59768-hpt5t
|
||||
namespace: istio-system
|
||||
labels:
|
||||
istio: galley
|
||||
spec:
|
||||
containers:
|
||||
- name: galley
|
||||
image: docker.io/istio/galley:1.1.8
|
||||
command:
|
||||
- "/usr/local/bin/galley"
|
||||
- server
|
||||
- "--meshConfigFile=/etc/mesh-config/mesh"
|
||||
- "--livenessProbeInterval=1s"
|
||||
- "--livenessProbePath=/healthliveness"
|
||||
- "--readinessProbePath=/healthready"
|
||||
- "--readinessProbeInterval=1s"
|
||||
- "--deployment-namespace=istio-system"
|
||||
- "--insecure=true"
|
||||
- "--validation-webhook-config-file"
|
||||
- "/etc/config/validatingwebhookconfiguration.yaml"
|
||||
- "--monitoringPort=15014"
|
||||
- "--log_output_level=default:info"
|
||||
ports:
|
||||
- containerPort: 443
|
||||
protocol: TCP
|
||||
- containerPort: 15014
|
||||
protocol: TCP
|
||||
- containerPort: 9901
|
||||
protocol: TCP`
|
||||
|
||||
testServiceYaml := `apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: pilot
|
||||
name: istio-pilot
|
||||
namespace: istio-system
|
||||
spec:
|
||||
clusterIP: 10.102.230.31
|
||||
ports:
|
||||
- name: grpc-xds
|
||||
port: 15010
|
||||
protocol: TCP
|
||||
targetPort: 15010
|
||||
- name: https-xds
|
||||
port: 15011
|
||||
protocol: TCP
|
||||
targetPort: 15011
|
||||
- name: http-legacy-discovery
|
||||
port: 8080
|
||||
protocol: TCP
|
||||
targetPort: 8080
|
||||
- name: http-monitoring
|
||||
port: 15014
|
||||
protocol: TCP
|
||||
targetPort: 15014
|
||||
selector:
|
||||
istio: pilot
|
||||
sessionAffinity: None
|
||||
type: ClusterIP`
|
||||
|
||||
parseK8sObjectsFromYAMLManifestTests := []struct {
|
||||
desc string
|
||||
objsMap map[string]string
|
||||
}{
|
||||
{
|
||||
"FromHybridYAMLManifest",
|
||||
map[string]string{
|
||||
"Deployment:istio-system:istio-citadel": testDeploymentYaml,
|
||||
"Pod:istio-system:istio-galley-75bcd59768-hpt5t": testPodYaml,
|
||||
"Service:istio-system:istio-pilot": testServiceYaml,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range parseK8sObjectsFromYAMLManifestTests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
testManifestYaml := strings.Join([]string{testDeploymentYaml, testPodYaml, testServiceYaml}, YAMLSeparator)
|
||||
gotK8sObjs, err := ParseK8sObjectsFromYAMLManifest(testManifestYaml)
|
||||
if err != nil {
|
||||
gotK8sObjsMap := gotK8sObjs.ToMap()
|
||||
for objHash, want := range tt.objsMap {
|
||||
if gotObj, ok := gotK8sObjsMap[objHash]; ok {
|
||||
gotObjYaml := gotObj.YAMLDebugString()
|
||||
if !util.IsYAMLEqual(gotObjYaml, want) {
|
||||
t.Errorf("ParseK8sObjectsFromYAMLManifest(%s): got:\n%s\n\nwant:\n%s\nDiff:\n%s\n", tt.desc, gotObjYaml, want, util.YAMLDiff(gotObjYaml, want))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestK8sObject_Equal(t *testing.T) {
|
||||
obj1 := K8sObject{
|
||||
object: &unstructured.Unstructured{Object: map[string]any{
|
||||
"key": "value1",
|
||||
}},
|
||||
}
|
||||
obj2 := K8sObject{
|
||||
object: &unstructured.Unstructured{Object: map[string]any{
|
||||
"key": "value2",
|
||||
}},
|
||||
}
|
||||
cases := []struct {
|
||||
desc string
|
||||
o1 *K8sObject
|
||||
o2 *K8sObject
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
desc: "Equals",
|
||||
o1: &obj1,
|
||||
o2: &obj1,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
desc: "NotEquals",
|
||||
o1: &obj1,
|
||||
o2: &obj2,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
desc: "NilSource",
|
||||
o1: nil,
|
||||
o2: &obj2,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
desc: "NilDest",
|
||||
o1: &obj1,
|
||||
o2: nil,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
desc: "TwoNils",
|
||||
o1: nil,
|
||||
o2: nil,
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
res := tt.o1.Equal(tt.o2)
|
||||
if res != tt.want {
|
||||
t.Errorf("got %v, want: %v", res, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestK8sObject_ResolveK8sConflict(t *testing.T) {
|
||||
getK8sObject := func(ystr string) *K8sObject {
|
||||
o, err := ParseYAMLToK8sObject([]byte(ystr))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Ensure that json data is in sync.
|
||||
// Since the object was created using yaml, json is empty.
|
||||
// make sure the object json is set correctly.
|
||||
o.json, _ = o.JSON()
|
||||
return o
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
o1 *K8sObject
|
||||
o2 *K8sObject
|
||||
}{
|
||||
{
|
||||
desc: "not applicable kind",
|
||||
o1: getK8sObject(`
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: pilot
|
||||
name: istio-pilot
|
||||
namespace: istio-system
|
||||
spec:
|
||||
clusterIP: 10.102.230.31`),
|
||||
o2: getK8sObject(`
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: pilot
|
||||
name: istio-pilot
|
||||
namespace: istio-system
|
||||
spec:
|
||||
clusterIP: 10.102.230.31`),
|
||||
},
|
||||
{
|
||||
desc: "only minAvailable is set",
|
||||
o1: getK8sObject(`
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: zk-pdb
|
||||
spec:
|
||||
minAvailable: 2`),
|
||||
o2: getK8sObject(`
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: zk-pdb
|
||||
spec:
|
||||
minAvailable: 2`),
|
||||
},
|
||||
{
|
||||
desc: "only maxUnavailable is set",
|
||||
o1: getK8sObject(`
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: istio
|
||||
spec:
|
||||
maxUnavailable: 3`),
|
||||
o2: getK8sObject(`
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: istio
|
||||
spec:
|
||||
maxUnavailable: 3`),
|
||||
},
|
||||
{
|
||||
desc: "minAvailable and maxUnavailable are set to none zero values",
|
||||
o1: getK8sObject(`
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: istio
|
||||
spec:
|
||||
maxUnavailable: 50%
|
||||
minAvailable: 3`),
|
||||
o2: getK8sObject(`
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: istio
|
||||
spec:
|
||||
maxUnavailable: 50%`),
|
||||
},
|
||||
{
|
||||
desc: "both minAvailable and maxUnavailable are set default",
|
||||
o1: getK8sObject(`
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: istio
|
||||
spec:
|
||||
minAvailable: 0
|
||||
maxUnavailable: 0`),
|
||||
o2: getK8sObject(`
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: istio
|
||||
spec:
|
||||
maxUnavailable: 0
|
||||
minAvailable: 0`),
|
||||
},
|
||||
}
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
newObj := tt.o1.ResolveK8sConflict()
|
||||
if !newObj.Equal(tt.o2) {
|
||||
newObjjson, _ := newObj.JSON()
|
||||
wantedObjjson, _ := tt.o2.JSON()
|
||||
t.Errorf("Got: %s, want: %s", string(newObjjson), string(wantedObjjson))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
287
pkg/cmd/hgctl/helm/profile.go
Normal file
287
pkg/cmd/hgctl/helm/profile.go
Normal file
@@ -0,0 +1,287 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package helm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
type InstallMode string
|
||||
|
||||
const (
|
||||
InstallK8s InstallMode = "k8s"
|
||||
InstallLocalK8s InstallMode = "local-k8s"
|
||||
InstallLocalDocker InstallMode = "local-docker"
|
||||
InstallLocal InstallMode = "local"
|
||||
)
|
||||
|
||||
type Profile struct {
|
||||
Profile string `json:"profile,omitempty"`
|
||||
InstallPackagePath string `json:"installPackagePath,omitempty"`
|
||||
Global ProfileGlobal `json:"global,omitempty"`
|
||||
Console ProfileConsole `json:"console,omitempty"`
|
||||
Gateway ProfileGateway `json:"gateway,omitempty"`
|
||||
Controller ProfileController `json:"controller,omitempty"`
|
||||
Storage ProfileStorage `json:"storage,omitempty"`
|
||||
Values map[string]any `json:"values,omitempty"`
|
||||
Charts ProfileCharts `json:"charts,omitempty"`
|
||||
}
|
||||
|
||||
type ProfileGlobal struct {
|
||||
Install InstallMode `json:"install,omitempty"`
|
||||
IngressClass string `json:"ingressClass,omitempty"`
|
||||
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"`
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
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"))
|
||||
}
|
||||
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"))
|
||||
}
|
||||
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"`
|
||||
}
|
||||
|
||||
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))
|
||||
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 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"))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
type ProfileGateway struct {
|
||||
Replicas uint32 `json:"replicas,omitempty"`
|
||||
HttpPort uint32 `json:"httpPort,omitempty"`
|
||||
HttpsPort uint32 `json:"httpsPort,omitempty"`
|
||||
MetricsPort uint32 `json:"metricsPort,omitempty"`
|
||||
}
|
||||
|
||||
func (p ProfileGateway) SetFlags(install InstallMode) ([]string, error) {
|
||||
sets := make([]string, 0)
|
||||
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"))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
type ProfileController struct {
|
||||
Replicas uint32 `json:"replicas,omitempty"`
|
||||
}
|
||||
|
||||
func (p ProfileController) SetFlags(install InstallMode) ([]string, error) {
|
||||
sets := make([]string, 0)
|
||||
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"))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
type ProfileStorage struct {
|
||||
Url string `json:"url,omitempty"`
|
||||
Ns string `json:"ns,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
DataEncKey string `json:"DataEncKey,omitempty"`
|
||||
}
|
||||
|
||||
func (p ProfileStorage) Validate(install InstallMode) []error {
|
||||
errs := make([]error, 0)
|
||||
return errs
|
||||
}
|
||||
|
||||
type Chart struct {
|
||||
Url string `json:"url,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
type ProfileCharts struct {
|
||||
Higress Chart `json:"higress,omitempty"`
|
||||
Istio Chart `json:"istio,omitempty"`
|
||||
}
|
||||
|
||||
func (p ProfileCharts) Validate(install InstallMode) []error {
|
||||
errs := make([]error, 0)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (p *Profile) ValuesYaml() (string, error) {
|
||||
setFlags := make([]string, 0)
|
||||
// Get global setting
|
||||
globalFlags, _ := p.Global.SetFlags(p.Global.Install)
|
||||
setFlags = append(setFlags, globalFlags...)
|
||||
|
||||
// Get console setting
|
||||
consoleFlags, _ := p.Console.SetFlags(p.Global.Install)
|
||||
setFlags = append(setFlags, consoleFlags...)
|
||||
|
||||
// Get gateway setting
|
||||
gatewayFlags, _ := p.Gateway.SetFlags(p.Global.Install)
|
||||
setFlags = append(setFlags, gatewayFlags...)
|
||||
|
||||
// Get controller setting
|
||||
controllerFlags, _ := p.Controller.SetFlags(p.Global.Install)
|
||||
setFlags = append(setFlags, controllerFlags...)
|
||||
|
||||
valueOverlayYAML := ""
|
||||
if p.Values != nil {
|
||||
out, err := yaml.Marshal(p.Values)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
valueOverlayYAML = string(out)
|
||||
}
|
||||
// merge values and setFlags
|
||||
overlayYAML, err := overlaySetFlagValues(valueOverlayYAML, setFlags)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return overlayYAML, nil
|
||||
}
|
||||
|
||||
func (p *Profile) IstioEnabled() bool {
|
||||
if (p.Global.Install == InstallK8s || p.Global.Install == InstallLocalK8s) && p.Global.EnableIstioAPI {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Profile) Validate() error {
|
||||
errs := make([]error, 0)
|
||||
errsGlobal := p.Global.Validate(p.Global.Install)
|
||||
if len(errsGlobal) > 0 {
|
||||
errs = append(errs, errsGlobal...)
|
||||
}
|
||||
errsConsole := p.Console.Validate(p.Global.Install)
|
||||
if len(errsConsole) > 0 {
|
||||
errs = append(errs, errsConsole...)
|
||||
}
|
||||
errsGateway := p.Gateway.Validate(p.Global.Install)
|
||||
if len(errsGateway) > 0 {
|
||||
errs = append(errs, errsGateway...)
|
||||
}
|
||||
errsController := p.Controller.Validate(p.Global.Install)
|
||||
if len(errsController) > 0 {
|
||||
errs = append(errs, errsController...)
|
||||
}
|
||||
errsStorage := p.Storage.Validate(p.Global.Install)
|
||||
if len(errsController) > 0 {
|
||||
errs = append(errs, errsStorage...)
|
||||
}
|
||||
errsCharts := p.Charts.Validate(p.Global.Install)
|
||||
if len(errsCharts) > 0 {
|
||||
errs = append(errs, errsCharts...)
|
||||
}
|
||||
|
||||
if len(errs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return errors.New(ToString(errs, "\n"))
|
||||
}
|
||||
|
||||
// ToString returns a string representation of errors, with elements separated by separator string. Any nil errors in the
|
||||
// slice are skipped.
|
||||
func ToString(errors []error, separator string) string {
|
||||
var out string
|
||||
for i, e := range errors {
|
||||
if e == nil {
|
||||
continue
|
||||
}
|
||||
if i != 0 {
|
||||
out += separator
|
||||
}
|
||||
out += e.Error()
|
||||
}
|
||||
return out
|
||||
}
|
||||
593
pkg/cmd/hgctl/helm/render.go
Normal file
593
pkg/cmd/hgctl/helm/render.go
Normal file
@@ -0,0 +1,593 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package helm
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/manifests"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/util"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"helm.sh/helm/v3/pkg/chart"
|
||||
"helm.sh/helm/v3/pkg/chart/loader"
|
||||
"helm.sh/helm/v3/pkg/chartutil"
|
||||
"helm.sh/helm/v3/pkg/cli"
|
||||
"helm.sh/helm/v3/pkg/downloader"
|
||||
"helm.sh/helm/v3/pkg/engine"
|
||||
"helm.sh/helm/v3/pkg/getter"
|
||||
"helm.sh/helm/v3/pkg/repo"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultProfileName is the name of the default profile for installation.
|
||||
DefaultProfileName = "local-k8s"
|
||||
// DefaultProfileFilename is the name of the default profile yaml file for installation.
|
||||
DefaultProfileFilename = "local-k8s.yaml"
|
||||
// DefaultUninstallProfileName is the name of the default profile yaml file for uninstallation.
|
||||
DefaultUninstallProfileName = "local-k8s"
|
||||
|
||||
// ChartsSubdirName = "charts"
|
||||
profilesRoot = "profiles"
|
||||
|
||||
RepoLatestVersion = "latest"
|
||||
RepoChartIndexYamlHigressIndex = "higress"
|
||||
|
||||
YAMLSeparator = "\n---\n"
|
||||
NotesFileNameSuffix = ".txt"
|
||||
)
|
||||
|
||||
func LoadValues(profileName string, chartsDir string) (string, error) {
|
||||
path := strings.Join([]string{profilesRoot, builtinProfileToFilename(profileName)}, "/")
|
||||
by, err := fs.ReadFile(manifests.BuiltinOrDir(chartsDir), path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(by), nil
|
||||
}
|
||||
|
||||
func readProfiles(chartsDir string) (map[string]bool, error) {
|
||||
profiles := map[string]bool{}
|
||||
f := manifests.BuiltinOrDir(chartsDir)
|
||||
dir, err := fs.ReadDir(f, profilesRoot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, f := range dir {
|
||||
if f.Name() == "_all.yaml" {
|
||||
continue
|
||||
}
|
||||
trimmedString := strings.TrimSuffix(f.Name(), ".yaml")
|
||||
if f.Name() != trimmedString {
|
||||
profiles[trimmedString] = true
|
||||
}
|
||||
}
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
func builtinProfileToFilename(name string) string {
|
||||
if name == "" {
|
||||
return DefaultProfileFilename
|
||||
}
|
||||
return name + ".yaml"
|
||||
}
|
||||
|
||||
// stripPrefix removes the given prefix from prefix.
|
||||
func stripPrefix(path, prefix string) string {
|
||||
pl := len(strings.Split(prefix, "/"))
|
||||
pv := strings.Split(path, "/")
|
||||
return strings.Join(pv[pl:], "/")
|
||||
}
|
||||
|
||||
// ListProfiles list all the profiles.
|
||||
func ListProfiles(charts string) ([]string, error) {
|
||||
profiles, err := readProfiles(charts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return util.StringBoolMapToSlice(profiles), nil
|
||||
}
|
||||
|
||||
var DefaultFilters = []util.FilterFunc{
|
||||
util.LicenseFilter,
|
||||
util.FormatterFilter,
|
||||
util.SpaceFilter,
|
||||
}
|
||||
|
||||
// Renderer is responsible for rendering helm chart with new values.
|
||||
type Renderer interface {
|
||||
Init() error
|
||||
RenderManifest(valsYaml string) (string, error)
|
||||
SetVersion(version string)
|
||||
}
|
||||
|
||||
type RendererOptions struct {
|
||||
Name string
|
||||
Namespace string
|
||||
|
||||
// fields for LocalRenderer
|
||||
FS fs.FS
|
||||
Dir string
|
||||
|
||||
// fields for RemoteRenderer
|
||||
Version string
|
||||
RepoURL string
|
||||
}
|
||||
|
||||
type RendererOption func(*RendererOptions)
|
||||
|
||||
func WithName(name string) RendererOption {
|
||||
return func(opts *RendererOptions) {
|
||||
opts.Name = name
|
||||
}
|
||||
}
|
||||
|
||||
func WithNamespace(ns string) RendererOption {
|
||||
return func(opts *RendererOptions) {
|
||||
opts.Namespace = ns
|
||||
}
|
||||
}
|
||||
|
||||
func WithFS(f fs.FS) RendererOption {
|
||||
return func(opts *RendererOptions) {
|
||||
opts.FS = f
|
||||
}
|
||||
}
|
||||
|
||||
func WithDir(dir string) RendererOption {
|
||||
return func(opts *RendererOptions) {
|
||||
opts.Dir = dir
|
||||
}
|
||||
}
|
||||
|
||||
func WithVersion(version string) RendererOption {
|
||||
return func(opts *RendererOptions) {
|
||||
opts.Version = version
|
||||
}
|
||||
}
|
||||
|
||||
func WithRepoURL(repo string) RendererOption {
|
||||
return func(opts *RendererOptions) {
|
||||
opts.RepoURL = repo
|
||||
}
|
||||
}
|
||||
|
||||
// LocalRenderer load chart from local file system
|
||||
type LocalRenderer struct {
|
||||
Opts *RendererOptions
|
||||
Chart *chart.Chart
|
||||
Started bool
|
||||
}
|
||||
|
||||
func (lr *LocalRenderer) Init() error {
|
||||
fileNames, err := getFileNames(lr.Opts.FS, lr.Opts.Dir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("chart of component %s doesn't exist", lr.Opts.Name)
|
||||
}
|
||||
return fmt.Errorf("getFileNames err: %s", err)
|
||||
}
|
||||
var files []*loader.BufferedFile
|
||||
for _, fileName := range fileNames {
|
||||
data, err := fs.ReadFile(lr.Opts.FS, fileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ReadFile %s err: %s", fileName, err)
|
||||
}
|
||||
// todo:// explain why we need to do this
|
||||
name := util.StripPrefix(fileName, lr.Opts.Dir)
|
||||
file := &loader.BufferedFile{
|
||||
Name: name,
|
||||
Data: data,
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
newChart, err := loader.LoadFiles(files)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load chart of component %s err: %s", lr.Opts.Name, err)
|
||||
}
|
||||
lr.Chart = newChart
|
||||
lr.Started = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lr *LocalRenderer) RenderManifest(valsYaml string) (string, error) {
|
||||
if !lr.Started {
|
||||
return "", errors.New("LocalRenderer has not been init")
|
||||
}
|
||||
return renderManifest(valsYaml, lr.Chart, true, lr.Opts, DefaultFilters...)
|
||||
}
|
||||
|
||||
func (lr *LocalRenderer) SetVersion(version string) {
|
||||
lr.Opts.Version = version
|
||||
}
|
||||
|
||||
func NewLocalRenderer(opts ...RendererOption) (Renderer, error) {
|
||||
newOpts := &RendererOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(newOpts)
|
||||
}
|
||||
|
||||
if err := verifyRendererOptions(newOpts); err != nil {
|
||||
return nil, fmt.Errorf("verify err: %s", err)
|
||||
}
|
||||
return &LocalRenderer{
|
||||
Opts: newOpts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type RemoteRenderer struct {
|
||||
Opts *RendererOptions
|
||||
Chart *chart.Chart
|
||||
Started bool
|
||||
}
|
||||
|
||||
func (rr *RemoteRenderer) initChartPathOptions() *action.ChartPathOptions {
|
||||
return &action.ChartPathOptions{
|
||||
RepoURL: rr.Opts.RepoURL,
|
||||
Version: rr.Opts.Version,
|
||||
}
|
||||
}
|
||||
|
||||
func (rr *RemoteRenderer) Init() error {
|
||||
cpOpts := rr.initChartPathOptions()
|
||||
settings := cli.New()
|
||||
// using release name as chart name by default
|
||||
cp, err := locateChart(cpOpts, rr.Opts.Name, settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check chart dependencies to make sure all are present in /charts
|
||||
chartRequested, err := loader.Load(cp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := verifyInstallable(chartRequested); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rr.Chart = chartRequested
|
||||
rr.Started = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rr *RemoteRenderer) SetVersion(version string) {
|
||||
rr.Opts.Version = version
|
||||
}
|
||||
|
||||
func (rr *RemoteRenderer) RenderManifest(valsYaml string) (string, error) {
|
||||
if !rr.Started {
|
||||
return "", errors.New("RemoteRenderer has not been init")
|
||||
}
|
||||
return renderManifest(valsYaml, rr.Chart, false, rr.Opts, DefaultFilters...)
|
||||
}
|
||||
|
||||
func NewRemoteRenderer(opts ...RendererOption) (Renderer, error) {
|
||||
newOpts := &RendererOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(newOpts)
|
||||
}
|
||||
|
||||
return &RemoteRenderer{
|
||||
Opts: newOpts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func verifyRendererOptions(opts *RendererOptions) error {
|
||||
if opts.Name == "" {
|
||||
return errors.New("missing component name for Renderer")
|
||||
}
|
||||
if opts.Namespace == "" {
|
||||
return errors.New("missing component namespace for Renderer")
|
||||
}
|
||||
if opts.FS == nil {
|
||||
return errors.New("missing chart FS for Renderer")
|
||||
}
|
||||
if opts.Dir == "" {
|
||||
return errors.New("missing chart dir for Renderer")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// read all files recursively under root path from a certain local file system
|
||||
func getFileNames(f fs.FS, root string) ([]string, error) {
|
||||
var fileNames []string
|
||||
if err := fs.WalkDir(f, root, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
fileNames = append(fileNames, path)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fileNames, nil
|
||||
}
|
||||
|
||||
func verifyInstallable(cht *chart.Chart) error {
|
||||
typ := cht.Metadata.Type
|
||||
if typ == "" || typ == "application" {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s chart %s is not installable", typ, cht.Name())
|
||||
}
|
||||
|
||||
func renderManifest(valsYaml string, cht *chart.Chart, builtIn bool, opts *RendererOptions, filters ...util.FilterFunc) (string, error) {
|
||||
valsMap := make(map[string]any)
|
||||
if err := yaml.Unmarshal([]byte(valsYaml), &valsMap); err != nil {
|
||||
return "", fmt.Errorf("unmarshal failed err: %s", err)
|
||||
}
|
||||
RelOpts := chartutil.ReleaseOptions{
|
||||
Name: opts.Name,
|
||||
Namespace: opts.Namespace,
|
||||
}
|
||||
// TODO need to specify k8s version
|
||||
caps := chartutil.DefaultCapabilities
|
||||
// maybe we need a configuration to change this caps
|
||||
resVals, err := chartutil.ToRenderValues(cht, valsMap, RelOpts, caps)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ToRenderValues failed err: %s", err)
|
||||
}
|
||||
if builtIn {
|
||||
resVals["Values"].(chartutil.Values)["enabled"] = true
|
||||
}
|
||||
filesMap, err := engine.Render(cht, resVals)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Render chart failed err: %s", err)
|
||||
}
|
||||
keys := make([]string, 0, len(filesMap))
|
||||
for key := range filesMap {
|
||||
// remove notation files such as Notes.txt
|
||||
if strings.HasSuffix(key, NotesFileNameSuffix) {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, key)
|
||||
}
|
||||
// to ensure that every manifest rendered by same values are the same
|
||||
sort.Strings(keys)
|
||||
|
||||
var builder strings.Builder
|
||||
for i := 0; i < len(keys); i++ {
|
||||
file := filesMap[keys[i]]
|
||||
file = util.ApplyFilters(file, filters...)
|
||||
// ignore empty manifest
|
||||
if file == "" {
|
||||
continue
|
||||
}
|
||||
if !strings.HasSuffix(file, YAMLSeparator) {
|
||||
file += YAMLSeparator
|
||||
}
|
||||
builder.WriteString(file)
|
||||
}
|
||||
|
||||
// render CRD
|
||||
crdFiles := cht.CRDObjects()
|
||||
// Sort crd files by name to ensure stable manifest output
|
||||
sort.Slice(crdFiles, func(i, j int) bool { return crdFiles[i].Name < crdFiles[j].Name })
|
||||
for _, crdFile := range crdFiles {
|
||||
f := string(crdFile.File.Data)
|
||||
// add yaml separator if the rendered file doesn't have one at the end
|
||||
f = strings.TrimSpace(f) + "\n"
|
||||
if !strings.HasSuffix(f, YAMLSeparator) {
|
||||
f += YAMLSeparator
|
||||
}
|
||||
builder.WriteString(f)
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
// locateChart locate the target chart path by sequential orders:
|
||||
// 1. find local helm repository using "name-version.tgz" format
|
||||
// 2. using downloader to pull remote chart
|
||||
func locateChart(cpOpts *action.ChartPathOptions, name string, settings *cli.EnvSettings) (string, error) {
|
||||
name = strings.TrimSpace(name)
|
||||
version := strings.TrimSpace(cpOpts.Version)
|
||||
|
||||
// check if it's in Helm's chart cache
|
||||
// cacheName is hardcoded as format of helm. eg: grafana-6.31.1.tgz
|
||||
cacheName := name + "-" + cpOpts.Version + ".tgz"
|
||||
cachePath := path.Join(settings.RepositoryCache, cacheName)
|
||||
if _, err := os.Stat(cachePath); err == nil {
|
||||
abs, err := filepath.Abs(cachePath)
|
||||
if err != nil {
|
||||
return abs, err
|
||||
}
|
||||
if cpOpts.Verify {
|
||||
if _, err := downloader.VerifyChart(abs, cpOpts.Keyring); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return abs, nil
|
||||
}
|
||||
|
||||
dl := downloader.ChartDownloader{
|
||||
Out: os.Stdout,
|
||||
Keyring: cpOpts.Keyring,
|
||||
Getters: getter.All(settings),
|
||||
Options: []getter.Option{
|
||||
getter.WithPassCredentialsAll(cpOpts.PassCredentialsAll),
|
||||
getter.WithTLSClientConfig(cpOpts.CertFile, cpOpts.KeyFile, cpOpts.CaFile),
|
||||
getter.WithInsecureSkipVerifyTLS(cpOpts.InsecureSkipTLSverify),
|
||||
},
|
||||
RepositoryConfig: settings.RepositoryConfig,
|
||||
RepositoryCache: settings.RepositoryCache,
|
||||
}
|
||||
|
||||
if cpOpts.Verify {
|
||||
dl.Verify = downloader.VerifyAlways
|
||||
}
|
||||
if cpOpts.RepoURL != "" {
|
||||
chartURL, err := repo.FindChartInAuthAndTLSAndPassRepoURL(cpOpts.RepoURL, cpOpts.Username, cpOpts.Password, name, version,
|
||||
cpOpts.CertFile, cpOpts.KeyFile, cpOpts.CaFile, cpOpts.InsecureSkipTLSverify, cpOpts.PassCredentialsAll, getter.All(settings))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
name = chartURL
|
||||
|
||||
// Only pass the user/pass on when the user has said to or when the
|
||||
// location of the chart repo and the chart are the same domain.
|
||||
u1, err := url.Parse(cpOpts.RepoURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
u2, err := url.Parse(chartURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Host on URL (returned from url.Parse) contains the port if present.
|
||||
// This check ensures credentials are not passed between different
|
||||
// services on different ports.
|
||||
if cpOpts.PassCredentialsAll || (u1.Scheme == u2.Scheme && u1.Host == u2.Host) {
|
||||
dl.Options = append(dl.Options, getter.WithBasicAuth(cpOpts.Username, cpOpts.Password))
|
||||
} else {
|
||||
dl.Options = append(dl.Options, getter.WithBasicAuth("", ""))
|
||||
}
|
||||
} else {
|
||||
dl.Options = append(dl.Options, getter.WithBasicAuth(cpOpts.Username, cpOpts.Password))
|
||||
}
|
||||
|
||||
// if RepositoryCache doesn't exist, create it
|
||||
if err := os.MkdirAll(settings.RepositoryCache, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
filename, _, err := dl.DownloadTo(name, version, settings.RepositoryCache)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
fileAbsPath, err := filepath.Abs(filename)
|
||||
if err != nil {
|
||||
return filename, err
|
||||
}
|
||||
return fileAbsPath, nil
|
||||
}
|
||||
|
||||
func ParseLatestVersion(repoUrl string, version string) (string, error) {
|
||||
|
||||
cpOpts := &action.ChartPathOptions{
|
||||
RepoURL: repoUrl,
|
||||
Version: version,
|
||||
}
|
||||
settings := cli.New()
|
||||
|
||||
indexURL, err := repo.ResolveReferenceURL(repoUrl, "index.yaml")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
u, err := url.Parse(repoUrl)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid chart URL format: %s", repoUrl)
|
||||
}
|
||||
|
||||
client, err := getter.All(settings).ByScheme(u.Scheme)
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not find protocol handler for: %s", u.Scheme)
|
||||
}
|
||||
|
||||
resp, err := client.Get(indexURL,
|
||||
getter.WithURL(cpOpts.RepoURL),
|
||||
getter.WithInsecureSkipVerifyTLS(cpOpts.InsecureSkipTLSverify),
|
||||
getter.WithTLSClientConfig(cpOpts.CertFile, cpOpts.KeyFile, cpOpts.CaFile),
|
||||
getter.WithBasicAuth(cpOpts.Username, cpOpts.Password),
|
||||
getter.WithPassCredentialsAll(cpOpts.PassCredentialsAll),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
index, err := io.ReadAll(resp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
indexFile, err := loadIndex(index)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// get higress helm chart latest version
|
||||
if entries, ok := indexFile.Entries[RepoChartIndexYamlHigressIndex]; ok {
|
||||
return entries[0].AppVersion, nil
|
||||
}
|
||||
|
||||
return "", errors.New("can't find higress latest version")
|
||||
}
|
||||
|
||||
// loadIndex loads an index file and does minimal validity checking.
|
||||
//
|
||||
// The source parameter is only used for logging.
|
||||
// This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails.
|
||||
func loadIndex(data []byte) (*repo.IndexFile, error) {
|
||||
i := &repo.IndexFile{}
|
||||
if len(data) == 0 {
|
||||
return i, errors.New("empty index.yaml file")
|
||||
}
|
||||
if err := jsonOrYamlUnmarshal(data, i); err != nil {
|
||||
return i, err
|
||||
}
|
||||
for _, cvs := range i.Entries {
|
||||
for idx := len(cvs) - 1; idx >= 0; idx-- {
|
||||
if cvs[idx] == nil {
|
||||
continue
|
||||
}
|
||||
if cvs[idx].APIVersion == "" {
|
||||
cvs[idx].APIVersion = chart.APIVersionV1
|
||||
}
|
||||
if err := cvs[idx].Validate(); err != nil {
|
||||
cvs = append(cvs[:idx], cvs[idx+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
i.SortEntries()
|
||||
if i.APIVersion == "" {
|
||||
return i, errors.New("no API version specified")
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// jsonOrYamlUnmarshal unmarshals the given byte slice containing JSON or YAML
|
||||
// into the provided interface.
|
||||
//
|
||||
// It automatically detects whether the data is in JSON or YAML format by
|
||||
// checking its validity as JSON. If the data is valid JSON, it will use the
|
||||
// `encoding/json` package to unmarshal it. Otherwise, it will use the
|
||||
// `sigs.k8s.io/yaml` package to unmarshal the YAML data.
|
||||
func jsonOrYamlUnmarshal(b []byte, i interface{}) error {
|
||||
if json.Valid(b) {
|
||||
return json.Unmarshal(b, i)
|
||||
}
|
||||
return yaml.UnmarshalStrict(b, i)
|
||||
}
|
||||
548
pkg/cmd/hgctl/helm/tpath/tree.go
Normal file
548
pkg/cmd/hgctl/helm/tpath/tree.go
Normal file
@@ -0,0 +1,548 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tpath
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/util"
|
||||
"gopkg.in/yaml.v2"
|
||||
yaml2 "sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// PathContext provides a means for traversing a tree towards the root.
|
||||
type PathContext struct {
|
||||
// Parent in the Parent of this PathContext.
|
||||
Parent *PathContext
|
||||
// KeyToChild is the key required to reach the child.
|
||||
KeyToChild any
|
||||
// Node is the actual Node in the data tree.
|
||||
Node any
|
||||
}
|
||||
|
||||
// String implements the Stringer interface.
|
||||
func (nc *PathContext) String() string {
|
||||
ret := "\n--------------- NodeContext ------------------\n"
|
||||
if nc.Parent != nil {
|
||||
ret += fmt.Sprintf("Parent.Node=\n%s\n", nc.Parent.Node)
|
||||
ret += fmt.Sprintf("KeyToChild=%v\n", nc.Parent.KeyToChild)
|
||||
}
|
||||
|
||||
ret += fmt.Sprintf("Node=\n%s\n", nc.Node)
|
||||
ret += "----------------------------------------------\n"
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// GetPathContext returns the PathContext for the Node which has the given path from root.
|
||||
// It returns false and no error if the given path is not found, or an error code in other error situations, like
|
||||
// a malformed path.
|
||||
// It also creates a tree of PathContexts during the traversal so that Parent nodes can be updated if required. This is
|
||||
// required when (say) appending to a list, where the parent list itself must be updated.
|
||||
func GetPathContext(root any, path util.Path, createMissing bool) (*PathContext, bool, error) {
|
||||
return getPathContext(&PathContext{Node: root}, path, path, createMissing)
|
||||
}
|
||||
|
||||
// WritePathContext writes the given value to the Node in the given PathContext.
|
||||
func WritePathContext(nc *PathContext, value any, merge bool) error {
|
||||
|
||||
if !util.IsValueNil(value) {
|
||||
return setPathContext(nc, value, merge)
|
||||
}
|
||||
|
||||
if nc.Parent == nil {
|
||||
return errors.New("cannot delete root element")
|
||||
}
|
||||
|
||||
switch {
|
||||
case isSliceOrPtrInterface(nc.Parent.Node):
|
||||
if err := util.DeleteFromSlicePtr(nc.Parent.Node, nc.Parent.KeyToChild.(int)); err != nil {
|
||||
return err
|
||||
}
|
||||
if isMapOrInterface(nc.Parent.Parent.Node) {
|
||||
return util.InsertIntoMap(nc.Parent.Parent.Node, nc.Parent.Parent.KeyToChild, nc.Parent.Node)
|
||||
}
|
||||
// TODO: The case of deleting a list.list.node element is not currently supported.
|
||||
return fmt.Errorf("cannot delete path: unsupported parent.parent type %T for delete", nc.Parent.Parent.Node)
|
||||
case util.IsMap(nc.Parent.Node):
|
||||
return util.DeleteFromMap(nc.Parent.Node, nc.Parent.KeyToChild)
|
||||
default:
|
||||
}
|
||||
return fmt.Errorf("cannot delete path: unsupported parent type %T for delete", nc.Parent.Node)
|
||||
}
|
||||
|
||||
// WriteNode writes value to the tree in root at the given path, creating any required missing internal nodes in path.
|
||||
func WriteNode(root any, path util.Path, value any) error {
|
||||
pc, _, err := getPathContext(&PathContext{Node: root}, path, path, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return WritePathContext(pc, value, false)
|
||||
}
|
||||
|
||||
// MergeNode merges value to the tree in root at the given path, creating any required missing internal nodes in path.
|
||||
func MergeNode(root any, path util.Path, value any) error {
|
||||
pc, _, err := getPathContext(&PathContext{Node: root}, path, path, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return WritePathContext(pc, value, true)
|
||||
}
|
||||
|
||||
// Find returns the value at path from the given tree, or false if the path does not exist.
|
||||
// It behaves differently from GetPathContext in that it never creates map entries at the leaf and does not provide
|
||||
// a way to mutate the parent of the found node.
|
||||
func Find(inputTree map[string]any, path util.Path) (any, bool, error) {
|
||||
if len(path) == 0 {
|
||||
return nil, false, fmt.Errorf("path is empty")
|
||||
}
|
||||
node, found := find(inputTree, path)
|
||||
return node, found, nil
|
||||
}
|
||||
|
||||
// Delete sets value at path of input untyped tree to nil
|
||||
func Delete(root map[string]any, path util.Path) (bool, error) {
|
||||
pc, _, err := getPathContext(&PathContext{Node: root}, path, path, false)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, WritePathContext(pc, nil, false)
|
||||
}
|
||||
|
||||
// getPathContext is the internal implementation of GetPathContext.
|
||||
// If createMissing is true, it creates any missing map (but NOT list) path entries in root.
|
||||
func getPathContext(nc *PathContext, fullPath, remainPath util.Path, createMissing bool) (*PathContext, bool, error) {
|
||||
if len(remainPath) == 0 {
|
||||
return nc, true, nil
|
||||
}
|
||||
pe := remainPath[0]
|
||||
|
||||
if nc.Node == nil {
|
||||
if !createMissing {
|
||||
return nil, false, fmt.Errorf("node %s is zero", pe)
|
||||
}
|
||||
if util.IsNPathElement(pe) || util.IsKVPathElement(pe) {
|
||||
nc.Node = []any{}
|
||||
} else {
|
||||
nc.Node = make(map[string]any)
|
||||
}
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(nc.Node)
|
||||
if v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
|
||||
v = v.Elem()
|
||||
}
|
||||
ncNode := v.Interface()
|
||||
|
||||
// For list types, we need a key to identify the selected list item. This can be either a value key of the
|
||||
// form :matching_value in the case of a leaf list, or a matching key:value in the case of a non-leaf list.
|
||||
if lst, ok := ncNode.([]any); ok {
|
||||
// If the path element has the form [N], a list element is being selected by index. Return the element at index
|
||||
// N if it exists.
|
||||
if util.IsNPathElement(pe) {
|
||||
idx, err := util.PathN(pe)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("path %s, index %s: %s", fullPath, pe, err)
|
||||
}
|
||||
var foundNode any
|
||||
if idx >= len(lst) || idx < 0 {
|
||||
if !createMissing {
|
||||
return nil, false, fmt.Errorf("index %d exceeds list length %d at path %s", idx, len(lst), remainPath)
|
||||
}
|
||||
idx = len(lst)
|
||||
foundNode = make(map[string]any)
|
||||
} else {
|
||||
foundNode = lst[idx]
|
||||
}
|
||||
nn := &PathContext{
|
||||
Parent: nc,
|
||||
Node: foundNode,
|
||||
}
|
||||
nc.KeyToChild = idx
|
||||
return getPathContext(nn, fullPath, remainPath[1:], createMissing)
|
||||
}
|
||||
|
||||
// Otherwise the path element must have form [key:value]. In this case, go through all list elements, which
|
||||
// must have map type, and try to find one which has a matching key:value.
|
||||
for idx, le := range lst {
|
||||
// non-leaf list, expect to match item by key:value.
|
||||
if lm, ok := le.(map[any]any); ok {
|
||||
k, v, err := util.PathKV(pe)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("path %s: %s", fullPath, err)
|
||||
}
|
||||
if stringsEqual(lm[k], v) {
|
||||
nn := &PathContext{
|
||||
Parent: nc,
|
||||
Node: lm,
|
||||
}
|
||||
nc.KeyToChild = idx
|
||||
nn.KeyToChild = k
|
||||
if len(remainPath) == 1 {
|
||||
return nn, true, nil
|
||||
}
|
||||
return getPathContext(nn, fullPath, remainPath[1:], createMissing)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// repeat of the block above for the case where tree unmarshals to map[string]interface{}. There doesn't
|
||||
// seem to be a way to merge this case into the above block.
|
||||
if lm, ok := le.(map[string]any); ok {
|
||||
k, v, err := util.PathKV(pe)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("path %s: %s", fullPath, err)
|
||||
}
|
||||
if stringsEqual(lm[k], v) {
|
||||
nn := &PathContext{
|
||||
Parent: nc,
|
||||
Node: lm,
|
||||
}
|
||||
nc.KeyToChild = idx
|
||||
nn.KeyToChild = k
|
||||
if len(remainPath) == 1 {
|
||||
return nn, true, nil
|
||||
}
|
||||
return getPathContext(nn, fullPath, remainPath[1:], createMissing)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// leaf list, expect path element [V], match based on value V.
|
||||
v, err := util.PathV(pe)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("path %s: %s", fullPath, err)
|
||||
}
|
||||
if matchesRegex(v, le) {
|
||||
nn := &PathContext{
|
||||
Parent: nc,
|
||||
Node: le,
|
||||
}
|
||||
nc.KeyToChild = idx
|
||||
return getPathContext(nn, fullPath, remainPath[1:], createMissing)
|
||||
}
|
||||
}
|
||||
return nil, false, fmt.Errorf("path %s: element %s not found", fullPath, pe)
|
||||
}
|
||||
|
||||
if util.IsMap(ncNode) {
|
||||
var nn any
|
||||
if m, ok := ncNode.(map[any]any); ok {
|
||||
nn, ok = m[pe]
|
||||
if !ok {
|
||||
// remainPath == 1 means the patch is creation of a new leaf.
|
||||
if createMissing || len(remainPath) == 1 {
|
||||
m[pe] = make(map[any]any)
|
||||
nn = m[pe]
|
||||
} else {
|
||||
return nil, false, fmt.Errorf("path not found at element %s in path %s", pe, fullPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
if reflect.ValueOf(ncNode).IsNil() {
|
||||
ncNode = make(map[string]any)
|
||||
nc.Node = ncNode
|
||||
}
|
||||
if m, ok := ncNode.(map[string]any); ok {
|
||||
nn, ok = m[pe]
|
||||
if !ok {
|
||||
// remainPath == 1 means the patch is creation of a new leaf.
|
||||
if createMissing || len(remainPath) == 1 {
|
||||
nextElementNPath := len(remainPath) > 1 && util.IsNPathElement(remainPath[1])
|
||||
if nextElementNPath {
|
||||
m[pe] = make([]any, 0)
|
||||
} else {
|
||||
m[pe] = make(map[string]any)
|
||||
}
|
||||
nn = m[pe]
|
||||
} else {
|
||||
return nil, false, fmt.Errorf("path not found at element %s in path %s", pe, fullPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
npc := &PathContext{
|
||||
Parent: nc,
|
||||
Node: nn,
|
||||
}
|
||||
// for slices, use the address so that the slice can be mutated.
|
||||
if util.IsSlice(nn) {
|
||||
npc.Node = &nn
|
||||
}
|
||||
nc.KeyToChild = pe
|
||||
return getPathContext(npc, fullPath, remainPath[1:], createMissing)
|
||||
}
|
||||
|
||||
return nil, false, fmt.Errorf("leaf type %T in non-leaf Node %s", nc.Node, remainPath)
|
||||
}
|
||||
|
||||
// setPathContext writes the given value to the Node in the given PathContext,
|
||||
// enlarging all PathContext lists to ensure all indexes are valid.
|
||||
func setPathContext(nc *PathContext, value any, merge bool) error {
|
||||
processParent, err := setValueContext(nc, value, merge)
|
||||
if err != nil || !processParent {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the path included insertions, process them now
|
||||
if nc.Parent.Parent == nil {
|
||||
return nil
|
||||
}
|
||||
return setPathContext(nc.Parent, nc.Parent.Node, false) // note: tail recursive
|
||||
}
|
||||
|
||||
// setValueContext writes the given value to the Node in the given PathContext.
|
||||
// If setting the value requires growing the final slice, grows it.
|
||||
func setValueContext(nc *PathContext, value any, merge bool) (bool, error) {
|
||||
if nc.Parent == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
vv, mapFromString := tryToUnmarshalStringToYAML(value)
|
||||
|
||||
switch parentNode := nc.Parent.Node.(type) {
|
||||
case *any:
|
||||
switch vParentNode := (*parentNode).(type) {
|
||||
case []any:
|
||||
idx := nc.Parent.KeyToChild.(int)
|
||||
if idx == -1 {
|
||||
// Treat -1 as insert-at-end of list
|
||||
idx = len(vParentNode)
|
||||
}
|
||||
|
||||
if idx >= len(vParentNode) {
|
||||
newElements := make([]any, idx-len(vParentNode)+1)
|
||||
vParentNode = append(vParentNode, newElements...)
|
||||
*parentNode = vParentNode
|
||||
}
|
||||
|
||||
merged, err := mergeConditional(vv, nc.Node, merge)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
vParentNode[idx] = merged
|
||||
nc.Node = merged
|
||||
default:
|
||||
return false, fmt.Errorf("don't know about vtype %T", vParentNode)
|
||||
}
|
||||
case map[string]any:
|
||||
key := nc.Parent.KeyToChild.(string)
|
||||
|
||||
// Update is treated differently depending on whether the value is a scalar or map type. If scalar,
|
||||
// insert a new element into the terminal node, otherwise replace the terminal node with the new subtree.
|
||||
if ncNode, ok := nc.Node.(*any); ok && !mapFromString {
|
||||
switch vNcNode := (*ncNode).(type) {
|
||||
case []any:
|
||||
switch vv.(type) {
|
||||
case map[string]any:
|
||||
// the vv is a map, and the node is a slice
|
||||
mergedValue := append(vNcNode, vv)
|
||||
parentNode[key] = mergedValue
|
||||
case *any:
|
||||
merged, err := mergeConditional(vv, vNcNode, merge)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
parentNode[key] = merged
|
||||
nc.Node = merged
|
||||
default:
|
||||
// the vv is an basic JSON type (int, float, string, bool)
|
||||
vv = append(vNcNode, vv)
|
||||
parentNode[key] = vv
|
||||
nc.Node = vv
|
||||
}
|
||||
default:
|
||||
return false, fmt.Errorf("don't know about vnc type %T", vNcNode)
|
||||
}
|
||||
} else {
|
||||
// For map passed as string type, the root is the new key.
|
||||
if mapFromString {
|
||||
if err := util.DeleteFromMap(nc.Parent.Node, nc.Parent.KeyToChild); err != nil {
|
||||
return false, err
|
||||
}
|
||||
vm := vv.(map[string]any)
|
||||
newKey := getTreeRoot(vm)
|
||||
return false, util.InsertIntoMap(nc.Parent.Node, newKey, vm[newKey])
|
||||
}
|
||||
parentNode[key] = vv
|
||||
nc.Node = vv
|
||||
}
|
||||
// TODO `map[interface{}]interface{}` is used by tests in operator/cmd/mesh, we should add our own tests
|
||||
case map[any]any:
|
||||
key := nc.Parent.KeyToChild.(string)
|
||||
parentNode[key] = vv
|
||||
nc.Node = vv
|
||||
default:
|
||||
return false, fmt.Errorf("don't know about type %T", parentNode)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// mergeConditional returns a merge of newVal and originalVal if merge is true, otherwise it returns newVal.
|
||||
func mergeConditional(newVal, originalVal any, merge bool) (any, error) {
|
||||
if !merge || util.IsValueNilOrDefault(originalVal) {
|
||||
return newVal, nil
|
||||
}
|
||||
newS, err := yaml.Marshal(newVal)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if util.IsYAMLEmpty(string(newS)) {
|
||||
return originalVal, nil
|
||||
}
|
||||
originalS, err := yaml.Marshal(originalVal)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if util.IsYAMLEmpty(string(originalS)) {
|
||||
return newVal, nil
|
||||
}
|
||||
|
||||
mergedS, err := util.OverlayYAML(string(originalS), string(newS))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if util.IsMap(originalVal) {
|
||||
// For JSON compatibility
|
||||
out := make(map[string]any)
|
||||
if err := yaml.Unmarshal([]byte(mergedS), &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
// For scalars and slices, copy the type
|
||||
out := originalVal
|
||||
if err := yaml.Unmarshal([]byte(mergedS), &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// find returns the value at path from the given tree, or false if the path does not exist.
|
||||
func find(treeNode any, path util.Path) (any, bool) {
|
||||
if len(path) == 0 || treeNode == nil {
|
||||
return nil, false
|
||||
}
|
||||
switch nt := treeNode.(type) {
|
||||
case map[any]any:
|
||||
val := nt[path[0]]
|
||||
if val == nil {
|
||||
return nil, false
|
||||
}
|
||||
if len(path) == 1 {
|
||||
return val, true
|
||||
}
|
||||
return find(val, path[1:])
|
||||
case map[string]any:
|
||||
val := nt[path[0]]
|
||||
if val == nil {
|
||||
return nil, false
|
||||
}
|
||||
if len(path) == 1 {
|
||||
return val, true
|
||||
}
|
||||
return find(val, path[1:])
|
||||
case []any:
|
||||
idx, err := strconv.Atoi(path[0])
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
if idx >= len(nt) {
|
||||
return nil, false
|
||||
}
|
||||
val := nt[idx]
|
||||
return find(val, path[1:])
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
// stringsEqual reports whether the string representations of a and b are equal. a and b may have different types.
|
||||
func stringsEqual(a, b any) bool {
|
||||
return fmt.Sprint(a) == fmt.Sprint(b)
|
||||
}
|
||||
|
||||
// matchesRegex reports whether str regex matches pattern.
|
||||
func matchesRegex(pattern, str any) bool {
|
||||
match, err := regexp.MatchString(fmt.Sprint(pattern), fmt.Sprint(str))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return match
|
||||
}
|
||||
|
||||
// isSliceOrPtrInterface reports whether v is a slice, a ptr to slice or interface to slice.
|
||||
func isSliceOrPtrInterface(v any) bool {
|
||||
vv := reflect.ValueOf(v)
|
||||
if vv.Kind() == reflect.Ptr {
|
||||
vv = vv.Elem()
|
||||
}
|
||||
if vv.Kind() == reflect.Interface {
|
||||
vv = vv.Elem()
|
||||
}
|
||||
return vv.Kind() == reflect.Slice
|
||||
}
|
||||
|
||||
// isMapOrInterface reports whether v is a map, or interface to a map.
|
||||
func isMapOrInterface(v any) bool {
|
||||
vv := reflect.ValueOf(v)
|
||||
if vv.Kind() == reflect.Interface {
|
||||
vv = vv.Elem()
|
||||
}
|
||||
return vv.Kind() == reflect.Map
|
||||
}
|
||||
|
||||
// tryToUnmarshalStringToYAML tries to unmarshal something that may be a YAML list or map into a structure. If not
|
||||
// possible, returns original scalar value.
|
||||
func tryToUnmarshalStringToYAML(s any) (any, bool) {
|
||||
// If value type is a string it could either be a literal string or a map type passed as a string. Try to unmarshal
|
||||
// to discover it's the latter.
|
||||
vv := s
|
||||
|
||||
if reflect.TypeOf(vv).Kind() == reflect.String {
|
||||
sv := strings.Split(vv.(string), "\n")
|
||||
// Need to be careful not to transform string literals into maps unless they really are maps, since scalar handling
|
||||
// is different for inserts.
|
||||
if len(sv) == 1 && strings.Contains(s.(string), ": ") ||
|
||||
len(sv) > 1 && strings.Contains(s.(string), ":") {
|
||||
nv := make(map[string]any)
|
||||
if err := json.Unmarshal([]byte(vv.(string)), &nv); err == nil {
|
||||
// treat JSON as string
|
||||
return vv, false
|
||||
}
|
||||
if err := yaml2.Unmarshal([]byte(vv.(string)), &nv); err == nil {
|
||||
return nv, true
|
||||
}
|
||||
}
|
||||
}
|
||||
// looks like a literal or failed unmarshal, return original type.
|
||||
return vv, false
|
||||
}
|
||||
|
||||
// getTreeRoot returns the first key found in m. It assumes a single root tree.
|
||||
func getTreeRoot(m map[string]any) string {
|
||||
for k := range m {
|
||||
return k
|
||||
}
|
||||
return ""
|
||||
}
|
||||
843
pkg/cmd/hgctl/helm/tpath/tree_test.go
Normal file
843
pkg/cmd/hgctl/helm/tpath/tree_test.go
Normal file
@@ -0,0 +1,843 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tpath
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/util"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
func TestWritePathContext(t *testing.T) {
|
||||
rootYAML := `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v1
|
||||
- name: n2
|
||||
list:
|
||||
- v1
|
||||
- v2
|
||||
- v3_regex
|
||||
`
|
||||
tests := []struct {
|
||||
desc string
|
||||
path string
|
||||
value any
|
||||
want string
|
||||
wantFound bool
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
desc: "AddListEntry",
|
||||
path: `a.b.[name:n2].list`,
|
||||
value: `foo`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v1
|
||||
- name: n2
|
||||
list:
|
||||
- v1
|
||||
- v2
|
||||
- v3_regex
|
||||
- foo
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "ModifyListEntryValue",
|
||||
path: `a.b.[name:n1].value`,
|
||||
value: `v2`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v2
|
||||
- list:
|
||||
- v1
|
||||
- v2
|
||||
- v3_regex
|
||||
name: n2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "ModifyListEntryValueQuoted",
|
||||
path: `a.b.[name:n1].value`,
|
||||
value: `v2`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: "n1"
|
||||
value: v2
|
||||
- list:
|
||||
- v1
|
||||
- v2
|
||||
- v3_regex
|
||||
name: n2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "ModifyListEntry",
|
||||
path: `a.b.[name:n2].list.[:v2]`,
|
||||
value: `v3`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v1
|
||||
- list:
|
||||
- v1
|
||||
- v3
|
||||
- v3_regex
|
||||
name: n2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "ModifyListEntryMapValue",
|
||||
path: `a.b.[name:n2]`,
|
||||
value: `name: n2
|
||||
list:
|
||||
- nk1: nv1
|
||||
- nk2: nv2`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v1
|
||||
- name: n2
|
||||
list:
|
||||
- nk1: nv1
|
||||
- nk2: nv2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "ModifyNthListEntry",
|
||||
path: `a.b.[1].list.[:v2]`,
|
||||
value: `v-the-second`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v1
|
||||
- list:
|
||||
- v1
|
||||
- v-the-second
|
||||
- v3_regex
|
||||
name: n2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "ModifyNthLeafListEntry",
|
||||
path: `a.b.[1].list.[2]`,
|
||||
value: `v-the-third`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v1
|
||||
- list:
|
||||
- v1
|
||||
- v2
|
||||
- v-the-third
|
||||
name: n2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "ModifyListEntryValueDotless",
|
||||
path: `a.b[name:n1].value`,
|
||||
value: `v2`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v2
|
||||
- list:
|
||||
- v1
|
||||
- v2
|
||||
- v3_regex
|
||||
name: n2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "DeleteListEntry",
|
||||
path: `a.b.[name:n1]`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- list:
|
||||
- v1
|
||||
- v2
|
||||
- v3_regex
|
||||
name: n2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "DeleteListEntryValue",
|
||||
path: `a.b.[name:n2].list.[:v2]`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v1
|
||||
- list:
|
||||
- v1
|
||||
- v3_regex
|
||||
name: n2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "DeleteListEntryIndex",
|
||||
path: `a.b.[name:n2].list.[1]`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v1
|
||||
- list:
|
||||
- v1
|
||||
- v3_regex
|
||||
name: n2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "DeleteListEntryValueRegex",
|
||||
path: `a.b.[name:n2].list.[:v3]`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v1
|
||||
- list:
|
||||
- v1
|
||||
- v2
|
||||
name: n2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "DeleteListLeafEntryBogusIndex",
|
||||
path: `a.b.[name:n2].list.[-200]`,
|
||||
wantFound: false,
|
||||
wantErr: `path a.b.[name:n2].list.[-200]: element [-200] not found`,
|
||||
},
|
||||
{
|
||||
desc: "DeleteListEntryBogusIndex",
|
||||
path: `a.b.[1000000].list.[:v2]`,
|
||||
wantFound: false,
|
||||
wantErr: `index 1000000 exceeds list length 2 at path [1000000].list.[:v2]`,
|
||||
},
|
||||
{
|
||||
desc: "AddMapEntry",
|
||||
path: `a.new_key`,
|
||||
value: `new_val`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v1
|
||||
- name: n2
|
||||
list:
|
||||
- v1
|
||||
- v2
|
||||
- v3_regex
|
||||
new_key: new_val
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "AddMapEntryMapValue",
|
||||
path: `a.new_key`,
|
||||
value: `new_key:
|
||||
nk1:
|
||||
nk2: nv2`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v1
|
||||
- name: n2
|
||||
list:
|
||||
- v1
|
||||
- v2
|
||||
- v3_regex
|
||||
new_key:
|
||||
nk1:
|
||||
nk2: nv2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "ModifyMapEntryMapValue",
|
||||
path: `a.b`,
|
||||
value: `nk1:
|
||||
nk2: nv2`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a:
|
||||
nk1:
|
||||
nk2: nv2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "DeleteMapEntry",
|
||||
path: `a.b`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
a: {}
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "path not found",
|
||||
path: `a.c.[name:n2].list.[:v3]`,
|
||||
wantFound: false,
|
||||
wantErr: `path not found at element c in path a.c.[name:n2].list.[:v3]`,
|
||||
},
|
||||
{
|
||||
desc: "error key",
|
||||
path: `a.b.[].list`,
|
||||
wantFound: false,
|
||||
wantErr: `path a.b.[].list: [] is not a valid key:value path element`,
|
||||
},
|
||||
{
|
||||
desc: "invalid index",
|
||||
path: `a.c.[n2].list.[:v3]`,
|
||||
wantFound: false,
|
||||
wantErr: `path not found at element c in path a.c.[n2].list.[:v3]`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
root := make(map[string]any)
|
||||
if err := yaml.Unmarshal([]byte(rootYAML), &root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pc, gotFound, gotErr := GetPathContext(root, util.PathFromString(tt.path), false)
|
||||
if gotErr, wantErr := errToString(gotErr), tt.wantErr; gotErr != wantErr {
|
||||
t.Fatalf("GetPathContext(%s): gotErr:%s, wantErr:%s", tt.desc, gotErr, wantErr)
|
||||
}
|
||||
if gotFound != tt.wantFound {
|
||||
t.Fatalf("GetPathContext(%s): gotFound:%v, wantFound:%v", tt.desc, gotFound, tt.wantFound)
|
||||
}
|
||||
if tt.wantErr != "" || !tt.wantFound {
|
||||
if tt.want != "" {
|
||||
t.Error("tt.want is set but never checked")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
err := WritePathContext(pc, tt.value, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
gotYAML := util.ToYAML(root)
|
||||
diff := util.YAMLDiff(gotYAML, tt.want)
|
||||
if diff != "" {
|
||||
t.Errorf("%s: (got:-, want:+):\n%s\n", tt.desc, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteNode(t *testing.T) {
|
||||
testTreeYAML := `
|
||||
a:
|
||||
b:
|
||||
c: val1
|
||||
list1:
|
||||
- i1: val1
|
||||
- i2: val2
|
||||
- i3a: key1
|
||||
i3b:
|
||||
list2:
|
||||
- i1: val1
|
||||
- i2: val2
|
||||
- i3a: key1
|
||||
i3b:
|
||||
i1: va11
|
||||
`
|
||||
tests := []struct {
|
||||
desc string
|
||||
baseYAML string
|
||||
path string
|
||||
value string
|
||||
want string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
desc: "insert empty",
|
||||
path: "a.b.c",
|
||||
value: "val1",
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
c: val1
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "overwrite",
|
||||
baseYAML: testTreeYAML,
|
||||
path: "a.b.c",
|
||||
value: "val2",
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
c: val2
|
||||
list1:
|
||||
- i1: val1
|
||||
- i2: val2
|
||||
- i3a: key1
|
||||
i3b:
|
||||
list2:
|
||||
- i1: val1
|
||||
- i2: val2
|
||||
- i3a: key1
|
||||
i3b:
|
||||
i1: va11
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "partial create",
|
||||
baseYAML: testTreeYAML,
|
||||
path: "a.b.d",
|
||||
value: "val3",
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
c: val1
|
||||
d: val3
|
||||
list1:
|
||||
- i1: val1
|
||||
- i2: val2
|
||||
- i3a: key1
|
||||
i3b:
|
||||
list2:
|
||||
- i1: val1
|
||||
- i2: val2
|
||||
- i3a: key1
|
||||
i3b:
|
||||
i1: va11
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "list keys",
|
||||
baseYAML: testTreeYAML,
|
||||
path: "a.b.list1.[i3a:key1].i3b.list2.[i3a:key1].i3b.i1",
|
||||
value: "val2",
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
c: val1
|
||||
list1:
|
||||
- i1: val1
|
||||
- i2: val2
|
||||
- i3a: key1
|
||||
i3b:
|
||||
list2:
|
||||
- i1: val1
|
||||
- i2: val2
|
||||
- i3a: key1
|
||||
i3b:
|
||||
i1: val2
|
||||
`,
|
||||
},
|
||||
// For https://github.com/istio/istio/issues/20950
|
||||
{
|
||||
desc: "with initial list",
|
||||
baseYAML: `
|
||||
components:
|
||||
ingressGateways:
|
||||
- enabled: true
|
||||
`,
|
||||
path: "components.ingressGateways[0].enabled",
|
||||
value: "false",
|
||||
want: `
|
||||
components:
|
||||
ingressGateways:
|
||||
- enabled: "false"
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "no initial list",
|
||||
baseYAML: "",
|
||||
path: "components.ingressGateways[0].enabled",
|
||||
value: "false",
|
||||
want: `
|
||||
components:
|
||||
ingressGateways:
|
||||
- enabled: "false"
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "no initial list for entry",
|
||||
baseYAML: `
|
||||
a: {}
|
||||
`,
|
||||
path: "a.list.[0]",
|
||||
value: "v1",
|
||||
want: `
|
||||
a:
|
||||
list:
|
||||
- v1
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "ExtendNthLeafListEntry",
|
||||
baseYAML: `
|
||||
a:
|
||||
list:
|
||||
- v1
|
||||
`,
|
||||
path: `a.list.[1]`,
|
||||
value: `v2`,
|
||||
want: `
|
||||
a:
|
||||
list:
|
||||
- v1
|
||||
- v2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "ExtendLeafListEntryLargeIndex",
|
||||
baseYAML: `
|
||||
a:
|
||||
list:
|
||||
- v1
|
||||
`,
|
||||
path: `a.list.[999]`,
|
||||
value: `v2`,
|
||||
want: `
|
||||
a:
|
||||
list:
|
||||
- v1
|
||||
- v2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "ExtendLeafListEntryNegativeIndex",
|
||||
baseYAML: `
|
||||
a:
|
||||
list:
|
||||
- v1
|
||||
`,
|
||||
path: `a.list.[-1]`,
|
||||
value: `v2`,
|
||||
want: `
|
||||
a:
|
||||
list:
|
||||
- v1
|
||||
- v2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "ExtendNthListEntry",
|
||||
baseYAML: `
|
||||
a:
|
||||
list:
|
||||
- name: foo
|
||||
`,
|
||||
path: `a.list.[1].name`,
|
||||
value: `bar`,
|
||||
want: `
|
||||
a:
|
||||
list:
|
||||
- name: foo
|
||||
- name: bar
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
root := make(map[string]any)
|
||||
if tt.baseYAML != "" {
|
||||
if err := yaml.Unmarshal([]byte(tt.baseYAML), &root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
p := util.PathFromString(tt.path)
|
||||
err := WriteNode(root, p, tt.value)
|
||||
if gotErr, wantErr := errToString(err), tt.wantErr; gotErr != wantErr {
|
||||
t.Errorf("%s: gotErr:%s, wantErr:%s", tt.desc, gotErr, wantErr)
|
||||
return
|
||||
}
|
||||
if got, want := util.ToYAML(root), tt.want; err == nil && util.YAMLDiff(got, want) != "" {
|
||||
t.Errorf("%s: got:\n%s\nwant:\n%s\ndiff:\n%s\n", tt.desc, got, want, util.YAMLDiff(got, want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeNode(t *testing.T) {
|
||||
testTreeYAML := `
|
||||
a:
|
||||
b:
|
||||
c: val1
|
||||
list1:
|
||||
- i1: val1
|
||||
- i2: val2
|
||||
`
|
||||
tests := []struct {
|
||||
desc string
|
||||
baseYAML string
|
||||
path string
|
||||
value string
|
||||
want string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
desc: "merge list entry",
|
||||
baseYAML: testTreeYAML,
|
||||
path: "a.b.list1.[i1:val1]",
|
||||
value: `
|
||||
i2b: val2`,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
c: val1
|
||||
list1:
|
||||
- i1: val1
|
||||
i2b: val2
|
||||
- i2: val2
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "merge list 2",
|
||||
baseYAML: testTreeYAML,
|
||||
path: "a.b.list1",
|
||||
value: `
|
||||
i3:
|
||||
a: val3
|
||||
`,
|
||||
want: `
|
||||
a:
|
||||
b:
|
||||
c: val1
|
||||
list1:
|
||||
- i1: val1
|
||||
- i2: val2
|
||||
- i3:
|
||||
a: val3
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
root := make(map[string]any)
|
||||
if tt.baseYAML != "" {
|
||||
if err := yaml.Unmarshal([]byte(tt.baseYAML), &root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
p := util.PathFromString(tt.path)
|
||||
iv := make(map[string]any)
|
||||
err := yaml.Unmarshal([]byte(tt.value), &iv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = MergeNode(root, p, iv)
|
||||
if gotErr, wantErr := errToString(err), tt.wantErr; gotErr != wantErr {
|
||||
t.Errorf("%s: gotErr:%s, wantErr:%s", tt.desc, gotErr, wantErr)
|
||||
return
|
||||
}
|
||||
if got, want := util.ToYAML(root), tt.want; err == nil && util.YAMLDiff(got, want) != "" {
|
||||
t.Errorf("%s: got:\n%s\nwant:\n%s\ndiff:\n%s\n", tt.desc, got, want, util.YAMLDiff(got, want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// errToString returns the string representation of err and the empty string if
|
||||
// err is nil.
|
||||
func errToString(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
// TestSecretVolumes simulates https://github.com/istio/istio/issues/20381
|
||||
func TestSecretVolumes(t *testing.T) {
|
||||
rootYAML := `
|
||||
values:
|
||||
gateways:
|
||||
istio-egressgateway:
|
||||
secretVolumes: []
|
||||
`
|
||||
root := make(map[string]any)
|
||||
if err := yaml.Unmarshal([]byte(rootYAML), &root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
overrides := []struct {
|
||||
path string
|
||||
value any
|
||||
}{
|
||||
{
|
||||
path: "values.gateways.istio-egressgateway.secretVolumes[0].name",
|
||||
value: "egressgateway-certs",
|
||||
},
|
||||
{
|
||||
path: "values.gateways.istio-egressgateway.secretVolumes[0].secretName",
|
||||
value: "istio-egressgateway-certs",
|
||||
},
|
||||
{
|
||||
path: "values.gateways.istio-egressgateway.secretVolumes[0].mountPath",
|
||||
value: "/etc/istio/egressgateway-certs",
|
||||
},
|
||||
{
|
||||
path: "values.gateways.istio-egressgateway.secretVolumes[1].name",
|
||||
value: "egressgateway-ca-certs",
|
||||
},
|
||||
{
|
||||
path: "values.gateways.istio-egressgateway.secretVolumes[1].secretName",
|
||||
value: "istio-egressgateway-ca-certs",
|
||||
},
|
||||
{
|
||||
path: "values.gateways.istio-egressgateway.secretVolumes[1].mountPath",
|
||||
value: "/etc/istio/egressgateway-ca-certs",
|
||||
},
|
||||
{
|
||||
path: "values.gateways.istio-egressgateway.secretVolumes[2].name",
|
||||
value: "nginx-client-certs",
|
||||
},
|
||||
{
|
||||
path: "values.gateways.istio-egressgateway.secretVolumes[2].secretName",
|
||||
value: "nginx-client-certs",
|
||||
},
|
||||
{
|
||||
path: "values.gateways.istio-egressgateway.secretVolumes[2].mountPath",
|
||||
value: "/etc/istio/nginx-client-certs",
|
||||
},
|
||||
{
|
||||
path: "values.gateways.istio-egressgateway.secretVolumes[3].name",
|
||||
value: "nginx-ca-certs",
|
||||
},
|
||||
{
|
||||
path: "values.gateways.istio-egressgateway.secretVolumes[3].secretName",
|
||||
value: "nginx-ca-certs",
|
||||
},
|
||||
{
|
||||
path: "values.gateways.istio-egressgateway.secretVolumes[3].mountPath",
|
||||
value: "/etc/istio/nginx-ca-certs",
|
||||
},
|
||||
}
|
||||
|
||||
for _, override := range overrides {
|
||||
|
||||
pc, _, err := GetPathContext(root, util.PathFromString(override.path), true)
|
||||
if err != nil {
|
||||
t.Fatalf("GetPathContext(%q): %v", override.path, err)
|
||||
}
|
||||
err = WritePathContext(pc, override.value, false)
|
||||
if err != nil {
|
||||
t.Fatalf("WritePathContext(%q): %v", override.path, err)
|
||||
}
|
||||
}
|
||||
|
||||
want := `
|
||||
values:
|
||||
gateways:
|
||||
istio-egressgateway:
|
||||
secretVolumes:
|
||||
- mountPath: /etc/istio/egressgateway-certs
|
||||
name: egressgateway-certs
|
||||
secretName: istio-egressgateway-certs
|
||||
- mountPath: /etc/istio/egressgateway-ca-certs
|
||||
name: egressgateway-ca-certs
|
||||
secretName: istio-egressgateway-ca-certs
|
||||
- mountPath: /etc/istio/nginx-client-certs
|
||||
name: nginx-client-certs
|
||||
secretName: nginx-client-certs
|
||||
- mountPath: /etc/istio/nginx-ca-certs
|
||||
name: nginx-ca-certs
|
||||
secretName: nginx-ca-certs
|
||||
`
|
||||
gotYAML := util.ToYAML(root)
|
||||
diff := util.YAMLDiff(gotYAML, want)
|
||||
if diff != "" {
|
||||
t.Errorf("TestSecretVolumes: diff:\n%s\n", diff)
|
||||
}
|
||||
}
|
||||
|
||||
// Simulates https://github.com/istio/istio/issues/19196
|
||||
func TestWriteEscapedPathContext(t *testing.T) {
|
||||
rootYAML := `
|
||||
values:
|
||||
sidecarInjectorWebhook:
|
||||
injectedAnnotations: {}
|
||||
`
|
||||
tests := []struct {
|
||||
desc string
|
||||
path string
|
||||
value any
|
||||
want string
|
||||
wantFound bool
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
desc: "ModifyEscapedPathValue",
|
||||
path: `values.sidecarInjectorWebhook.injectedAnnotations.container\.apparmor\.security\.beta\.kubernetes\.io/istio-proxy`,
|
||||
value: `runtime/default`,
|
||||
wantFound: true,
|
||||
want: `
|
||||
values:
|
||||
sidecarInjectorWebhook:
|
||||
injectedAnnotations:
|
||||
container.apparmor.security.beta.kubernetes.io/istio-proxy: runtime/default
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
root := make(map[string]any)
|
||||
if err := yaml.Unmarshal([]byte(rootYAML), &root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pc, gotFound, gotErr := GetPathContext(root, util.PathFromString(tt.path), false)
|
||||
if gotErr, wantErr := errToString(gotErr), tt.wantErr; gotErr != wantErr {
|
||||
t.Fatalf("GetPathContext(%s): gotErr:%s, wantErr:%s", tt.desc, gotErr, wantErr)
|
||||
}
|
||||
if gotFound != tt.wantFound {
|
||||
t.Fatalf("GetPathContext(%s): gotFound:%v, wantFound:%v", tt.desc, gotFound, tt.wantFound)
|
||||
}
|
||||
if tt.wantErr != "" || !tt.wantFound {
|
||||
return
|
||||
}
|
||||
|
||||
err := WritePathContext(pc, tt.value, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
gotYAML := util.ToYAML(root)
|
||||
diff := util.YAMLDiff(gotYAML, tt.want)
|
||||
if diff != "" {
|
||||
t.Errorf("%s: diff:\n%s\n", tt.desc, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
58
pkg/cmd/hgctl/helm/tpath/util.go
Normal file
58
pkg/cmd/hgctl/helm/tpath/util.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tpath
|
||||
|
||||
import (
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/util"
|
||||
"gopkg.in/yaml.v2"
|
||||
yaml2 "sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// AddSpecRoot adds a root node called "spec" to the given tree and returns the resulting tree.
|
||||
func AddSpecRoot(tree string) (string, error) {
|
||||
t, nt := make(map[string]any), make(map[string]any)
|
||||
if err := yaml.Unmarshal([]byte(tree), &t); err != nil {
|
||||
return "", err
|
||||
}
|
||||
nt["spec"] = t
|
||||
out, err := yaml.Marshal(nt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
// GetSpecSubtree returns the subtree under "spec".
|
||||
func GetSpecSubtree(yml string) (string, error) {
|
||||
return GetConfigSubtree(yml, "spec")
|
||||
}
|
||||
|
||||
// GetConfigSubtree returns the subtree at the given path.
|
||||
func GetConfigSubtree(manifest, path string) (string, error) {
|
||||
root := make(map[string]any)
|
||||
if err := yaml2.Unmarshal([]byte(manifest), &root); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
nc, _, err := GetPathContext(root, util.PathFromString(path), false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
out, err := yaml2.Marshal(nc.Node)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
122
pkg/cmd/hgctl/helm/tpath/util_test.go
Normal file
122
pkg/cmd/hgctl/helm/tpath/util_test.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package tpath
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAddSpecRoot(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in string
|
||||
expect string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty",
|
||||
in: ``,
|
||||
expect: `spec: {}
|
||||
`,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "add-root",
|
||||
in: `
|
||||
a: va
|
||||
b: foo`,
|
||||
expect: `spec:
|
||||
a: va
|
||||
b: foo
|
||||
`,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "err",
|
||||
in: `i can't be yaml, can I?`,
|
||||
expect: ``,
|
||||
err: errors.New(""),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got, err := AddSpecRoot(tt.in); got != tt.expect ||
|
||||
((err != nil && tt.err == nil) || (err == nil && tt.err != nil)) {
|
||||
t.Errorf("%s AddSpecRoot(%s) => %s, want %s", tt.desc, tt.in, got, tt.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetConfigSubtree(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
manifest string
|
||||
path string
|
||||
expect string
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
desc: "empty",
|
||||
manifest: ``,
|
||||
path: ``,
|
||||
expect: `{}
|
||||
`,
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "subtree",
|
||||
manifest: `
|
||||
a:
|
||||
b:
|
||||
- name: n1
|
||||
value: v2
|
||||
- list:
|
||||
- v1
|
||||
- v2
|
||||
- v3_regex
|
||||
name: n2
|
||||
`,
|
||||
path: `a`,
|
||||
expect: `b:
|
||||
- name: n1
|
||||
value: v2
|
||||
- list:
|
||||
- v1
|
||||
- v2
|
||||
- v3_regex
|
||||
name: n2
|
||||
`,
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "err",
|
||||
manifest: "not-yaml",
|
||||
path: "not-subnode",
|
||||
expect: ``,
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got, err := GetConfigSubtree(tt.manifest, tt.path); got != tt.expect || (err == nil) == tt.err {
|
||||
t.Errorf("%s GetConfigSubtree(%s, %s) => %s, want %s", tt.desc, tt.manifest, tt.path, got, tt.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
202
pkg/cmd/hgctl/install.go
Normal file
202
pkg/cmd/hgctl/install.go
Normal file
@@ -0,0 +1,202 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hgctl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/installer"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
|
||||
"github.com/alibaba/higress/pkg/cmd/options"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
setFlagHelpStr = `Override an higress profile value, e.g. to choose a profile
|
||||
(--set profile=local-k8s), or override profile values (--set gateway.replicas=2), or override helm values (--set values.global.proxy.resources.requsts.cpu=500m).`
|
||||
// manifestsFlagHelpStr is the command line description for --manifests
|
||||
manifestsFlagHelpStr = `Specify a path to a directory of profiles
|
||||
(e.g. ~/Downloads/higress/manifests).`
|
||||
outputHelpstr = "Specify a file to write profile yaml"
|
||||
|
||||
profileNameK8s = "k8s"
|
||||
profileNameLocalK8s = "local-k8s"
|
||||
)
|
||||
|
||||
type InstallArgs struct {
|
||||
InFilenames []string
|
||||
// KubeConfigPath is the path to kube config file.
|
||||
KubeConfigPath string
|
||||
// Context is the cluster context in the kube config
|
||||
Context string
|
||||
// Set is a string with element format "path=value" where path is an profile path and the value is a
|
||||
// value to set the node at that path to.
|
||||
Set []string
|
||||
// ManifestsPath is a path to a ManifestsPath and profiles directory in the local filesystem with a release tgz.
|
||||
ManifestsPath string
|
||||
}
|
||||
|
||||
func (a *InstallArgs) String() string {
|
||||
var b strings.Builder
|
||||
b.WriteString("KubeConfigPath: " + a.KubeConfigPath + "\n")
|
||||
b.WriteString("Context: " + a.Context + "\n")
|
||||
b.WriteString("Set: " + fmt.Sprint(a.Set) + "\n")
|
||||
b.WriteString("ManifestsPath: " + a.ManifestsPath + "\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func addInstallFlags(cmd *cobra.Command, args *InstallArgs) {
|
||||
cmd.PersistentFlags().StringArrayVarP(&args.Set, "set", "s", nil, setFlagHelpStr)
|
||||
cmd.PersistentFlags().StringVarP(&args.ManifestsPath, "manifests", "d", "", manifestsFlagHelpStr)
|
||||
}
|
||||
|
||||
// --manifests is an alias for --set installPackagePath=
|
||||
func applyFlagAliases(flags []string, manifestsPath string) []string {
|
||||
if manifestsPath != "" {
|
||||
flags = append(flags, fmt.Sprintf("installPackagePath=%s", manifestsPath))
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
// newInstallCmd generates a higress install manifest and applies it to a cluster
|
||||
func newInstallCmd() *cobra.Command {
|
||||
iArgs := &InstallArgs{}
|
||||
installCmd := &cobra.Command{
|
||||
Use: "install",
|
||||
Short: "Applies an higress manifest, installing or reconfiguring higress on a cluster.",
|
||||
Long: "The install command generates an higress install manifest and applies it to a cluster.",
|
||||
// nolint: lll
|
||||
Example: ` # Apply a default higress installation
|
||||
hgctl install
|
||||
|
||||
# Install higress on local kubernetes cluster
|
||||
hgctl install --set profile=local-k8s
|
||||
|
||||
# To override profile setting
|
||||
hgctl install --set profile=local-k8s --set global.enableIstioAPI=true --set gateway.replicas=2"
|
||||
|
||||
# To override helm setting
|
||||
hgctl install --set profile=local-k8s --set values.global.proxy.resources.requsts.cpu=500m"
|
||||
|
||||
`,
|
||||
Args: cobra.ExactArgs(0),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return Install(cmd.OutOrStdout(), iArgs)
|
||||
},
|
||||
}
|
||||
addInstallFlags(installCmd, iArgs)
|
||||
flags := installCmd.Flags()
|
||||
options.AddKubeConfigFlags(flags)
|
||||
return installCmd
|
||||
}
|
||||
|
||||
func Install(writer io.Writer, iArgs *InstallArgs) error {
|
||||
setFlags := applyFlagAliases(iArgs.Set, iArgs.ManifestsPath)
|
||||
|
||||
// check profileName
|
||||
psf := helm.GetValueForSetFlag(setFlags, "profile")
|
||||
if len(psf) == 0 {
|
||||
psf = promptProfileName(writer)
|
||||
setFlags = append(setFlags, fmt.Sprintf("profile=%s", psf))
|
||||
}
|
||||
|
||||
if !promptInstall(writer, psf) {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, profile, profileName, err := helm.GenerateConfig(iArgs.InFilenames, setFlags)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate config: %v", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(writer, "🧐 Validating Profile: \"%s\" \n", profileName)
|
||||
err = profile.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = InstallManifests(profile, writer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to install manifests: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func promptInstall(writer io.Writer, profileName string) bool {
|
||||
answer := ""
|
||||
for {
|
||||
fmt.Fprintf(writer, "\nThis will install Higress \"%s\" profile into the cluster. \nProceed? (y/N)", profileName)
|
||||
fmt.Scanln(&answer)
|
||||
if strings.TrimSpace(answer) == "y" {
|
||||
fmt.Fprintf(writer, "\n")
|
||||
return true
|
||||
}
|
||||
if strings.TrimSpace(answer) == "N" {
|
||||
fmt.Fprintf(writer, "Cancelled.\n")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func promptProfileName(writer io.Writer) string {
|
||||
answer := ""
|
||||
fmt.Fprintf(writer, "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")
|
||||
for {
|
||||
fmt.Fprintf(writer, "Please input 1 or 2 to select, input your selection:")
|
||||
fmt.Scanln(&answer)
|
||||
if strings.TrimSpace(answer) == "1" {
|
||||
return profileNameLocalK8s
|
||||
}
|
||||
if strings.TrimSpace(answer) == "2" {
|
||||
return profileNameK8s
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(writer, "\n⌛️ Processing installation... \n\n")
|
||||
if err := op.ApplyManifests(manifestMap); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(writer, "\n🎊 Install All Resources Complete!\n")
|
||||
return nil
|
||||
}
|
||||
112
pkg/cmd/hgctl/installer/component.go
Normal file
112
pkg/cmd/hgctl/installer/component.go
Normal file
@@ -0,0 +1,112 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package installer
|
||||
|
||||
import (
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/util"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
type ComponentName string
|
||||
|
||||
var ComponentMap = map[ComponentName]struct{}{
|
||||
Higress: {},
|
||||
Istio: {},
|
||||
}
|
||||
|
||||
type Component interface {
|
||||
// ComponentName returns the name of the component.
|
||||
ComponentName() ComponentName
|
||||
// Namespace returns the namespace for the component.
|
||||
Namespace() string
|
||||
// Enabled reports whether the component is enabled.
|
||||
Enabled() bool
|
||||
// Run starts the component. Must be called before the component is used.
|
||||
Run() error
|
||||
RenderManifest() (string, error)
|
||||
}
|
||||
|
||||
type ComponentOptions struct {
|
||||
Name string
|
||||
Namespace string
|
||||
// local
|
||||
ChartPath string
|
||||
// remote
|
||||
RepoURL string
|
||||
ChartName string
|
||||
Version string
|
||||
Quiet bool
|
||||
}
|
||||
|
||||
type ComponentOption func(*ComponentOptions)
|
||||
|
||||
func WithComponentNamespace(namespace string) ComponentOption {
|
||||
return func(opts *ComponentOptions) {
|
||||
opts.Namespace = namespace
|
||||
}
|
||||
}
|
||||
|
||||
func WithComponentChartPath(path string) ComponentOption {
|
||||
return func(opts *ComponentOptions) {
|
||||
opts.ChartPath = path
|
||||
}
|
||||
}
|
||||
|
||||
func WithComponentChartName(chartName string) ComponentOption {
|
||||
return func(opts *ComponentOptions) {
|
||||
opts.ChartName = chartName
|
||||
}
|
||||
}
|
||||
|
||||
func WithComponentRepoURL(url string) ComponentOption {
|
||||
return func(opts *ComponentOptions) {
|
||||
opts.RepoURL = url
|
||||
}
|
||||
}
|
||||
|
||||
func WithComponentVersion(version string) ComponentOption {
|
||||
return func(opts *ComponentOptions) {
|
||||
opts.Version = version
|
||||
}
|
||||
}
|
||||
|
||||
func WithQuiet() ComponentOption {
|
||||
return func(opts *ComponentOptions) {
|
||||
opts.Quiet = true
|
||||
}
|
||||
}
|
||||
|
||||
func renderComponentManifest(spec any, renderer helm.Renderer, addOn bool, name ComponentName, namespace string) (string, error) {
|
||||
var valsBytes []byte
|
||||
var valsYaml string
|
||||
var err error
|
||||
if yamlString, ok := spec.(string); ok {
|
||||
valsYaml = yamlString
|
||||
} else {
|
||||
if !util.IsValueNil(spec) {
|
||||
valsBytes, err = yaml.Marshal(spec)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
valsYaml = string(valsBytes)
|
||||
}
|
||||
}
|
||||
final, err := renderer.RenderManifest(valsYaml)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return final, nil
|
||||
}
|
||||
131
pkg/cmd/hgctl/installer/higress.go
Normal file
131
pkg/cmd/hgctl/installer/higress.go
Normal file
@@ -0,0 +1,131 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package installer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
|
||||
)
|
||||
|
||||
const (
|
||||
Higress ComponentName = "higress"
|
||||
)
|
||||
|
||||
type HigressComponent struct {
|
||||
profile *helm.Profile
|
||||
started bool
|
||||
opts *ComponentOptions
|
||||
renderer helm.Renderer
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
func (h *HigressComponent) ComponentName() ComponentName {
|
||||
return Higress
|
||||
}
|
||||
|
||||
func (h *HigressComponent) Namespace() string {
|
||||
return h.opts.Namespace
|
||||
}
|
||||
|
||||
func (h *HigressComponent) Enabled() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *HigressComponent) Run() error {
|
||||
// Parse latest version
|
||||
if h.opts.Version == helm.RepoLatestVersion {
|
||||
|
||||
latestVersion, err := helm.ParseLatestVersion(h.opts.RepoURL, h.opts.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !h.opts.Quiet {
|
||||
fmt.Fprintf(h.writer, "⚡️ Fetching Higress Helm Chart latest version \"%s\" \n", latestVersion)
|
||||
}
|
||||
|
||||
// Reset Helm Chart version
|
||||
h.opts.Version = latestVersion
|
||||
h.renderer.SetVersion(latestVersion)
|
||||
}
|
||||
if !h.opts.Quiet {
|
||||
fmt.Fprintf(h.writer, "🏄 Downloading Higress Helm Chart version: %s, url: %s\n", h.opts.Version, h.opts.RepoURL)
|
||||
}
|
||||
if err := h.renderer.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
h.started = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HigressComponent) RenderManifest() (string, error) {
|
||||
if !h.started {
|
||||
return "", nil
|
||||
}
|
||||
if !h.opts.Quiet {
|
||||
fmt.Fprintf(h.writer, "📦 Rendering Higress Helm Chart\n")
|
||||
}
|
||||
valsYaml, err := h.profile.ValuesYaml()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
manifest, err2 := renderComponentManifest(valsYaml, h.renderer, true, h.ComponentName(), h.opts.Namespace)
|
||||
if err2 != nil {
|
||||
return "", err
|
||||
}
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
func NewHigressComponent(profile *helm.Profile, writer io.Writer, opts ...ComponentOption) (Component, error) {
|
||||
newOpts := &ComponentOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(newOpts)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
higressComponent := &HigressComponent{
|
||||
profile: profile,
|
||||
renderer: renderer,
|
||||
opts: newOpts,
|
||||
writer: writer,
|
||||
}
|
||||
return higressComponent, nil
|
||||
}
|
||||
213
pkg/cmd/hgctl/installer/installer.go
Normal file
213
pkg/cmd/hgctl/installer/installer.go
Normal file
@@ -0,0 +1,213 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package installer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/helm/object"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
|
||||
)
|
||||
|
||||
type Installer struct {
|
||||
started bool
|
||||
components map[ComponentName]Component
|
||||
kubeCli kubernetes.CLIClient
|
||||
profile *helm.Profile
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
// 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()
|
||||
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 *Installer) GenerateManifests(manifestMap map[ComponentName]string) error {
|
||||
if o.kubeCli == nil {
|
||||
return errors.New("no injected k8s cli into Installer")
|
||||
}
|
||||
for _, manifest := range manifestMap {
|
||||
fmt.Fprint(o.writer, manifest)
|
||||
}
|
||||
return 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)
|
||||
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 *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)
|
||||
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 *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...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("NewHigressComponent failed, err: %s", 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())
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
113
pkg/cmd/hgctl/installer/istio.go
Normal file
113
pkg/cmd/hgctl/installer/istio.go
Normal file
@@ -0,0 +1,113 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package installer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
|
||||
)
|
||||
|
||||
const (
|
||||
Istio ComponentName = "istio"
|
||||
)
|
||||
|
||||
type IstioCRDComponent struct {
|
||||
profile *helm.Profile
|
||||
started bool
|
||||
opts *ComponentOptions
|
||||
renderer helm.Renderer
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
func NewIstioCRDComponent(profile *helm.Profile, writer io.Writer, opts ...ComponentOption) (Component, error) {
|
||||
newOpts := &ComponentOptions{}
|
||||
for _, opt := range opts {
|
||||
opt(newOpts)
|
||||
}
|
||||
|
||||
var renderer helm.Renderer
|
||||
var err error
|
||||
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(Istio)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
istioComponent := &IstioCRDComponent{
|
||||
profile: profile,
|
||||
renderer: renderer,
|
||||
opts: newOpts,
|
||||
writer: writer,
|
||||
}
|
||||
return istioComponent, nil
|
||||
}
|
||||
|
||||
func (i *IstioCRDComponent) ComponentName() ComponentName {
|
||||
return Istio
|
||||
}
|
||||
|
||||
func (i *IstioCRDComponent) Namespace() string {
|
||||
return i.opts.Namespace
|
||||
}
|
||||
|
||||
func (i *IstioCRDComponent) Enabled() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (i *IstioCRDComponent) Run() error {
|
||||
if !i.opts.Quiet {
|
||||
fmt.Fprintf(i.writer, "🏄 Downloading Istio Helm Chart version: %s, url: %s\n", i.opts.Version, i.opts.RepoURL)
|
||||
}
|
||||
if err := i.renderer.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
i.started = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *IstioCRDComponent) RenderManifest() (string, error) {
|
||||
if !i.started {
|
||||
return "", nil
|
||||
}
|
||||
if !i.opts.Quiet {
|
||||
fmt.Fprintf(i.writer, "📦 Rendering Istio Helm Chart\n")
|
||||
}
|
||||
values := make(map[string]any)
|
||||
manifest, err := renderComponentManifest(values, i.renderer, false, i.ComponentName(), i.opts.Namespace)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return manifest, nil
|
||||
}
|
||||
@@ -19,17 +19,21 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
kubescheme "k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
"k8s.io/client-go/util/retry"
|
||||
ctrClient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
type CLIClient interface {
|
||||
@@ -44,6 +48,15 @@ type CLIClient interface {
|
||||
|
||||
// PodExec takes a command and the pod data to run the command in the specified pod.
|
||||
PodExec(namespacedName types.NamespacedName, container string, command string) (stdout string, stderr string, err error)
|
||||
|
||||
// ApplyObject creates or updates unstructured object
|
||||
ApplyObject(obj *unstructured.Unstructured) error
|
||||
|
||||
// DeleteObject delete unstructured object
|
||||
DeleteObject(obj *unstructured.Unstructured) error
|
||||
|
||||
// CreateNamespace create namespace
|
||||
CreateNamespace(namespace string) error
|
||||
}
|
||||
|
||||
var _ CLIClient = &client{}
|
||||
@@ -52,6 +65,7 @@ type client struct {
|
||||
config *rest.Config
|
||||
restClient *rest.RESTClient
|
||||
kube kubernetes.Interface
|
||||
ctrClient ctrClient.Client
|
||||
}
|
||||
|
||||
func NewCLIClient(clientConfig clientcmd.ClientConfig) (CLIClient, error) {
|
||||
@@ -80,33 +94,13 @@ func newClientInternal(clientConfig clientcmd.ClientConfig) (*client, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.ctrClient, err = ctrClient.New(c.config, ctrClient.Options{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, err
|
||||
}
|
||||
|
||||
func setRestDefaults(config *rest.Config) *rest.Config {
|
||||
if config.GroupVersion == nil || config.GroupVersion.Empty() {
|
||||
config.GroupVersion = &corev1.SchemeGroupVersion
|
||||
}
|
||||
if len(config.APIPath) == 0 {
|
||||
if len(config.GroupVersion.Group) == 0 {
|
||||
config.APIPath = "/api"
|
||||
} else {
|
||||
config.APIPath = "/apis"
|
||||
}
|
||||
}
|
||||
if len(config.ContentType) == 0 {
|
||||
config.ContentType = runtime.ContentTypeJSON
|
||||
}
|
||||
if config.NegotiatedSerializer == nil {
|
||||
// This codec factory ensures the resources are not converted. Therefore, resources
|
||||
// will not be round-tripped through internal versions. Defaulting does not happen
|
||||
// on the client.
|
||||
config.NegotiatedSerializer = serializer.NewCodecFactory(kubescheme.Scheme).WithoutConversion()
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func (c *client) RESTConfig() *rest.Config {
|
||||
if c.config == nil {
|
||||
return nil
|
||||
@@ -170,3 +164,85 @@ func (c *client) PodExec(namespacedName types.NamespacedName, container string,
|
||||
stderr = stderrBuf.String()
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteObject delete unstructured object
|
||||
func (c *client) DeleteObject(obj *unstructured.Unstructured) error {
|
||||
err := c.ctrClient.Delete(context.TODO(), obj, ctrClient.PropagationPolicy(metav1.DeletePropagationBackground))
|
||||
if err != nil {
|
||||
if !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyObject creates or updates unstructured object
|
||||
func (c *client) ApplyObject(obj *unstructured.Unstructured) error {
|
||||
if obj.GetKind() == "List" {
|
||||
objList, err := obj.ToList()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range objList.Items {
|
||||
if err := c.ApplyObject(&item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
key := ctrClient.ObjectKeyFromObject(obj)
|
||||
receiver := &unstructured.Unstructured{}
|
||||
receiver.SetGroupVersionKind(obj.GroupVersionKind())
|
||||
|
||||
if err := retry.RetryOnConflict(wait.Backoff{
|
||||
Duration: time.Millisecond * 10,
|
||||
Factor: 2,
|
||||
Steps: 3,
|
||||
}, func() error {
|
||||
if err := c.ctrClient.Get(context.Background(), key, receiver); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
if err := c.ctrClient.Create(context.Background(), obj); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := applyOverlay(receiver, obj); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.ctrClient.Update(context.Background(), receiver); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateNamespace create namespace
|
||||
func (c *client) CreateNamespace(namespace string) error {
|
||||
key := ctrClient.ObjectKey{
|
||||
Namespace: metav1.NamespaceSystem,
|
||||
Name: namespace,
|
||||
}
|
||||
if err := c.ctrClient.Get(context.Background(), key, &corev1.Namespace{}); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
nsObj := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: metav1.NamespaceSystem,
|
||||
Name: namespace,
|
||||
},
|
||||
}
|
||||
if err := c.ctrClient.Create(context.Background(), nsObj); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to check if namespace %v exists: %v", namespace, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
157
pkg/cmd/hgctl/kubernetes/common.go
Normal file
157
pkg/cmd/hgctl/kubernetes/common.go
Normal file
@@ -0,0 +1,157 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch/v5"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
kubescheme "k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
)
|
||||
|
||||
// applyOverlay applies an overlay using JSON patch strategy over the current Object in place.
|
||||
func applyOverlay(current, overlay *unstructured.Unstructured) error {
|
||||
cj, err := runtime.Encode(unstructured.UnstructuredJSONScheme, current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
overlayUpdated := overlay.DeepCopy()
|
||||
if strings.EqualFold(current.GetKind(), "service") {
|
||||
if err := saveClusterIP(current, overlayUpdated); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
saveNodePorts(current, overlayUpdated)
|
||||
}
|
||||
|
||||
if current.GetKind() == "PersistentVolumeClaim" {
|
||||
if err := savePersistentVolumeClaim(current, overlayUpdated); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
uj, err := runtime.Encode(unstructured.UnstructuredJSONScheme, overlayUpdated)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
merged, err := jsonpatch.MergePatch(cj, uj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runtime.DecodeInto(unstructured.UnstructuredJSONScheme, merged, current)
|
||||
}
|
||||
|
||||
// createPortMap returns a map, mapping the value of the port and value of the nodePort
|
||||
func createPortMap(current *unstructured.Unstructured) map[string]uint32 {
|
||||
portMap := make(map[string]uint32)
|
||||
svc := &corev1.Service{}
|
||||
if err := scheme.Scheme.Convert(current, svc, nil); err != nil {
|
||||
return portMap
|
||||
}
|
||||
for _, p := range svc.Spec.Ports {
|
||||
portMap[strconv.Itoa(int(p.Port))] = uint32(p.NodePort)
|
||||
}
|
||||
return portMap
|
||||
}
|
||||
|
||||
// savePersistentVolumeClaim copies the storageClassName from the current cluster into the overlay
|
||||
func savePersistentVolumeClaim(current, overlay *unstructured.Unstructured) error {
|
||||
// Save the value of spec.storageClassName set by the cluster
|
||||
if storageClassName, found, err := unstructured.NestedString(current.Object, "spec",
|
||||
"storageClassName"); err != nil {
|
||||
return err
|
||||
} else if found {
|
||||
if _, _, err2 := unstructured.NestedString(overlay.Object, "spec",
|
||||
"storageClassName"); err2 != nil {
|
||||
// override when overlay storageClassName property is not existed
|
||||
if err3 := unstructured.SetNestedField(overlay.Object, storageClassName, "spec",
|
||||
"storageClassName"); err3 != nil {
|
||||
return err3
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// saveNodePorts transfers the port values from the current cluster into the overlay
|
||||
func saveNodePorts(current, overlay *unstructured.Unstructured) {
|
||||
portMap := createPortMap(current)
|
||||
ports, _, _ := unstructured.NestedFieldNoCopy(overlay.Object, "spec", "ports")
|
||||
portList, ok := ports.([]any)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for _, port := range portList {
|
||||
m, ok := port.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if nodePortNum, ok := m["nodePort"]; ok && fmt.Sprintf("%v", nodePortNum) == "0" {
|
||||
if portNum, ok := m["port"]; ok {
|
||||
if v, ok := portMap[fmt.Sprintf("%v", portNum)]; ok {
|
||||
m["nodePort"] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// saveClusterIP copies the cluster IP from the current cluster into the overlay
|
||||
func saveClusterIP(current, overlay *unstructured.Unstructured) error {
|
||||
// Save the value of spec.clusterIP set by the cluster
|
||||
if clusterIP, found, err := unstructured.NestedString(current.Object, "spec",
|
||||
"clusterIP"); err != nil {
|
||||
return err
|
||||
} else if found {
|
||||
if err := unstructured.SetNestedField(overlay.Object, clusterIP, "spec",
|
||||
"clusterIP"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setRestDefaults(config *rest.Config) *rest.Config {
|
||||
if config.GroupVersion == nil || config.GroupVersion.Empty() {
|
||||
config.GroupVersion = &corev1.SchemeGroupVersion
|
||||
}
|
||||
if len(config.APIPath) == 0 {
|
||||
if len(config.GroupVersion.Group) == 0 {
|
||||
config.APIPath = "/api"
|
||||
} else {
|
||||
config.APIPath = "/apis"
|
||||
}
|
||||
}
|
||||
if len(config.ContentType) == 0 {
|
||||
config.ContentType = runtime.ContentTypeJSON
|
||||
}
|
||||
if config.NegotiatedSerializer == nil {
|
||||
// This codec factory ensures the resources are not converted. Therefore, resources
|
||||
// will not be round-tripped through internal versions. Defaulting does not happen
|
||||
// on the client.
|
||||
config.NegotiatedSerializer = serializer.NewCodecFactory(kubescheme.Scheme).WithoutConversion()
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
@@ -48,6 +48,9 @@ type PortForwarder interface {
|
||||
|
||||
// Address returns the address of the local forwarded address.
|
||||
Address() string
|
||||
|
||||
// WaitForStop blocks until connection closed (e.g. control-C interrupt)
|
||||
WaitForStop()
|
||||
}
|
||||
|
||||
var _ PortForwarder = &localForwarder{}
|
||||
@@ -153,3 +156,7 @@ func (f *localForwarder) Stop() {
|
||||
func (f *localForwarder) Address() string {
|
||||
return fmt.Sprintf("%s:%d", DefaultLocalAddress, f.localPort)
|
||||
}
|
||||
|
||||
func (f *localForwarder) WaitForStop() {
|
||||
<-f.stopCh
|
||||
}
|
||||
|
||||
143
pkg/cmd/hgctl/manifest.go
Normal file
143
pkg/cmd/hgctl/manifest.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 hgctl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/installer"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
|
||||
"github.com/alibaba/higress/pkg/cmd/options"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type ManifestArgs struct {
|
||||
InFilenames []string
|
||||
// KubeConfigPath is the path to kube config file.
|
||||
KubeConfigPath string
|
||||
// Context is the cluster context in the kube config
|
||||
Context string
|
||||
// Set is a string with element format "path=value" where path is an profile path and the value is a
|
||||
// value to set the node at that path to.
|
||||
Set []string
|
||||
// ManifestsPath is a path to a ManifestsPath and profiles directory in the local filesystem with a release tgz.
|
||||
ManifestsPath string
|
||||
}
|
||||
|
||||
func (a *ManifestArgs) String() string {
|
||||
var b strings.Builder
|
||||
b.WriteString("KubeConfigPath: " + a.KubeConfigPath + "\n")
|
||||
b.WriteString("Context: " + a.Context + "\n")
|
||||
b.WriteString("Set: " + fmt.Sprint(a.Set) + "\n")
|
||||
b.WriteString("ManifestsPath: " + a.ManifestsPath + "\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// newManifestCmd generates a higress install manifest and applies it to a cluster
|
||||
func newManifestCmd() *cobra.Command {
|
||||
iArgs := &ManifestArgs{}
|
||||
manifestCmd := &cobra.Command{
|
||||
Use: "manifest",
|
||||
Short: "Generate higress manifests.",
|
||||
Long: "The manifest command generates an higress install manifests.",
|
||||
}
|
||||
|
||||
generate := newManifestGenerateCmd(iArgs)
|
||||
addManifestFlags(generate, iArgs)
|
||||
manifestCmd.AddCommand(generate)
|
||||
|
||||
return manifestCmd
|
||||
}
|
||||
|
||||
func addManifestFlags(cmd *cobra.Command, args *ManifestArgs) {
|
||||
cmd.PersistentFlags().StringArrayVarP(&args.Set, "set", "s", nil, setFlagHelpStr)
|
||||
cmd.PersistentFlags().StringVarP(&args.ManifestsPath, "manifests", "d", "", manifestsFlagHelpStr)
|
||||
}
|
||||
|
||||
// newManifestGenerateCmd generates a higress install manifest and applies it to a cluster
|
||||
func newManifestGenerateCmd(iArgs *ManifestArgs) *cobra.Command {
|
||||
installCmd := &cobra.Command{
|
||||
Use: "generate",
|
||||
Short: "Generate higress manifests.",
|
||||
Long: "The manifest generate command generates higress install manifests.",
|
||||
// nolint: lll
|
||||
Example: ` # Generate higress manifests
|
||||
hgctl manifest generate
|
||||
`,
|
||||
Args: cobra.ExactArgs(0),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return generate(cmd.OutOrStdout(), iArgs)
|
||||
},
|
||||
}
|
||||
|
||||
return installCmd
|
||||
}
|
||||
|
||||
func generate(writer io.Writer, iArgs *ManifestArgs) error {
|
||||
setFlags := applyFlagAliases(iArgs.Set, iArgs.ManifestsPath)
|
||||
|
||||
// check profileName
|
||||
psf := helm.GetValueForSetFlag(setFlags, "profile")
|
||||
if len(psf) == 0 {
|
||||
setFlags = append(setFlags, fmt.Sprintf("profile=%s", helm.InstallLocalK8s))
|
||||
}
|
||||
|
||||
_, profile, _, err := helm.GenerateConfig(iArgs.InFilenames, setFlags)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate config: %v", err)
|
||||
}
|
||||
|
||||
err = profile.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = genManifests(profile, writer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to install manifests: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func genManifests(profile *helm.Profile, writer io.Writer) error {
|
||||
cliClient, err := kubernetes.NewCLIClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build kubernetes client: %w", err)
|
||||
}
|
||||
|
||||
op, err := installer.NewInstaller(profile, cliClient, writer, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := op.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifestMap, err := op.RenderManifests()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := op.GenerateManifests(manifestMap); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
35
pkg/cmd/hgctl/manifests/manifest.go
Normal file
35
pkg/cmd/hgctl/manifests/manifest.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 manifests
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"os"
|
||||
)
|
||||
|
||||
// FS embeds the manifests
|
||||
//
|
||||
//go:embed profiles/*
|
||||
var FS embed.FS
|
||||
|
||||
// BuiltinOrDir returns a FS for the provided directory. If no directory is passed, the compiled in
|
||||
// FS will be used
|
||||
func BuiltinOrDir(dir string) fs.FS {
|
||||
if dir == "" {
|
||||
return FS
|
||||
}
|
||||
return os.DirFS(dir)
|
||||
}
|
||||
65
pkg/cmd/hgctl/manifests/profiles/_all.yaml
Normal file
65
pkg/cmd/hgctl/manifests/profiles/_all.yaml
Normal file
@@ -0,0 +1,65 @@
|
||||
# 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
|
||||
global:
|
||||
install: local # install mode k8s/local/docker
|
||||
ingressClass: higress
|
||||
watchNamespace:
|
||||
disableAlpnH2: true
|
||||
enableStatus: true
|
||||
enableIstioAPI: true
|
||||
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
|
||||
httpPort: 80
|
||||
httpsPort: 443
|
||||
metricsPort: 15020
|
||||
|
||||
controller:
|
||||
replicas: 1
|
||||
|
||||
storage:
|
||||
url: nacos://192.168.0.1:8848 # file://opt/higress/conf, buildin://127.0.0.1:8848
|
||||
ns: higress-system
|
||||
username:
|
||||
password:
|
||||
dataEncKey:
|
||||
|
||||
# values passed through to helm
|
||||
values:
|
||||
|
||||
|
||||
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
|
||||
53
pkg/cmd/hgctl/manifests/profiles/k8s.yaml
Normal file
53
pkg/cmd/hgctl/manifests/profiles/k8s.yaml
Normal file
@@ -0,0 +1,53 @@
|
||||
# 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
|
||||
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
|
||||
|
||||
controller:
|
||||
replicas: 1
|
||||
|
||||
# values passed through to helm
|
||||
values:
|
||||
|
||||
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
|
||||
53
pkg/cmd/hgctl/manifests/profiles/local-k8s.yaml
Normal file
53
pkg/cmd/hgctl/manifests/profiles/local-k8s.yaml
Normal file
@@ -0,0 +1,53 @@
|
||||
# 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
|
||||
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
|
||||
|
||||
controller:
|
||||
replicas: 1
|
||||
|
||||
# values passed through to helm
|
||||
values:
|
||||
|
||||
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
|
||||
44
pkg/cmd/hgctl/profile.go
Normal file
44
pkg/cmd/hgctl/profile.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hgctl
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ProfileCmd is a group of commands related to profile listing, dumping and diffing.
|
||||
func newProfileCmd() *cobra.Command {
|
||||
pc := &cobra.Command{
|
||||
Use: "profile",
|
||||
Short: "Commands related to higress configuration profiles",
|
||||
Long: "The profile command lists, dumps higress configuration profiles.",
|
||||
Example: "hgctl profile list\n" +
|
||||
"hgctl install --set profile=local-k8s # Use a profile from the list",
|
||||
}
|
||||
|
||||
pdArgs := &profileDumpArgs{}
|
||||
plArgs := &profileListArgs{}
|
||||
|
||||
plc := profileListCmd(plArgs)
|
||||
pdc := profileDumpCmd(pdArgs)
|
||||
|
||||
addProfileDumpFlags(pdc, pdArgs)
|
||||
addProfileListFlags(plc, plArgs)
|
||||
|
||||
pc.AddCommand(plc)
|
||||
pc.AddCommand(pdc)
|
||||
|
||||
return pc
|
||||
}
|
||||
72
pkg/cmd/hgctl/profile_dump.go
Normal file
72
pkg/cmd/hgctl/profile_dump.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hgctl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type profileDumpArgs struct {
|
||||
// output write profile to file
|
||||
output string
|
||||
// manifestsPath is a path to a charts and profiles directory in the local filesystem with a release tgz.
|
||||
manifestsPath string
|
||||
}
|
||||
|
||||
func addProfileDumpFlags(cmd *cobra.Command, args *profileDumpArgs) {
|
||||
cmd.PersistentFlags().StringVarP(&args.output, "output", "o", "", outputHelpstr)
|
||||
cmd.PersistentFlags().StringVarP(&args.manifestsPath, "manifests", "d", "", manifestsFlagHelpStr)
|
||||
}
|
||||
|
||||
func profileDumpCmd(pdArgs *profileDumpArgs) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "dump [<profile>]",
|
||||
Short: "Dumps a higress configuration profile",
|
||||
Long: "The dump subcommand dumps the values in a higress configuration profile.",
|
||||
Args: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) > 1 {
|
||||
return fmt.Errorf("too many positional arguments")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return profileDump(cmd, args, pdArgs)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func profileDump(cmd *cobra.Command, args []string, pdArgs *profileDumpArgs) error {
|
||||
profileName := helm.DefaultProfileName
|
||||
if len(args) == 1 {
|
||||
profileName = args[0]
|
||||
}
|
||||
yaml, err := helm.ReadProfileYAML(profileName, pdArgs.manifestsPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(pdArgs.output) > 0 {
|
||||
err2 := os.WriteFile(pdArgs.output, []byte(yaml), 0644)
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
} else {
|
||||
cmd.Println(yaml)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
63
pkg/cmd/hgctl/profile_list.go
Normal file
63
pkg/cmd/hgctl/profile_list.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hgctl
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type profileListArgs struct {
|
||||
// manifestsPath is a path to a charts and profiles directory in the local filesystem with a release tgz.
|
||||
manifestsPath string
|
||||
}
|
||||
|
||||
func addProfileListFlags(cmd *cobra.Command, args *profileListArgs) {
|
||||
cmd.PersistentFlags().StringVarP(&args.manifestsPath, "manifests", "d", "", manifestsFlagHelpStr)
|
||||
}
|
||||
|
||||
func profileListCmd(plArgs *profileListArgs) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "Lists available higress configuration profiles",
|
||||
Long: "The list subcommand lists the available higress configuration profiles.",
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return profileList(cmd, plArgs)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// profileList list all the builtin profiles.
|
||||
func profileList(cmd *cobra.Command, plArgs *profileListArgs) error {
|
||||
|
||||
profiles, err := helm.ListProfiles(plArgs.manifestsPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(profiles) == 0 {
|
||||
cmd.Println("No profiles available.")
|
||||
} else {
|
||||
cmd.Println("higress configuration profiles:")
|
||||
sort.Strings(profiles)
|
||||
for _, profile := range profiles {
|
||||
cmd.Printf(" %s\n", profile)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -28,6 +28,12 @@ func GetRootCommand() *cobra.Command {
|
||||
|
||||
rootCmd.AddCommand(newVersionCommand())
|
||||
rootCmd.AddCommand(newConfigCommand())
|
||||
rootCmd.AddCommand(newInstallCmd())
|
||||
rootCmd.AddCommand(newUninstallCmd())
|
||||
rootCmd.AddCommand(newUpgradeCmd())
|
||||
rootCmd.AddCommand(newProfileCmd())
|
||||
rootCmd.AddCommand(newDashboardCmd())
|
||||
rootCmd.AddCommand(newManifestCmd())
|
||||
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
136
pkg/cmd/hgctl/uninstall.go
Normal file
136
pkg/cmd/hgctl/uninstall.go
Normal file
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hgctl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/helm"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/installer"
|
||||
"github.com/alibaba/higress/pkg/cmd/hgctl/kubernetes"
|
||||
"github.com/alibaba/higress/pkg/cmd/options"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type 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,
|
||||
"Delete all of Istio resources")
|
||||
}
|
||||
|
||||
// newUninstallCmd command uninstalls Istio from a cluster
|
||||
func newUninstallCmd() *cobra.Command {
|
||||
uiArgs := &uninstallArgs{}
|
||||
uninstallCmd := &cobra.Command{
|
||||
Use: "uninstall",
|
||||
Short: "Uninstall higress from a cluster",
|
||||
Long: "The uninstall command uninstalls higress from a cluster",
|
||||
Example: ` # Uninstall higress
|
||||
hgctl uninstall
|
||||
|
||||
# Uninstall higress by special namespace
|
||||
hgctl uninstall --namespace=higress-system
|
||||
|
||||
# Uninstall higress and istio CRD
|
||||
hgctl uninstall --purge-istio-crd --istio-namespace=istio-system`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return uninstall(cmd.OutOrStdout(), uiArgs)
|
||||
},
|
||||
}
|
||||
addUninstallFlags(uninstallCmd, uiArgs)
|
||||
flags := uninstallCmd.Flags()
|
||||
options.AddKubeConfigFlags(flags)
|
||||
return uninstallCmd
|
||||
}
|
||||
|
||||
// uninstall uninstalls control plane by either pruning by target revision or deleting specified manifests.
|
||||
func uninstall(writer io.Writer, uiArgs *uninstallArgs) error {
|
||||
setFlags := make([]string, 0)
|
||||
profileName := helm.GetUninstallProfileName()
|
||||
_, profile, err := helm.GenProfile(profileName, "", setFlags)
|
||||
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 err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func promptUninstall(writer io.Writer) bool {
|
||||
answer := ""
|
||||
for {
|
||||
fmt.Fprintf(writer, "All Higress resources will be uninstalled 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 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()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(writer, "\n⌛️ Processing uninstallation... \n\n")
|
||||
if err := op.DeleteManifests(manifestMap); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(writer, "\n🎊 Uninstall All Resources Complete!\n")
|
||||
return nil
|
||||
}
|
||||
49
pkg/cmd/hgctl/upgrade.go
Normal file
49
pkg/cmd/hgctl/upgrade.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hgctl
|
||||
|
||||
import (
|
||||
"github.com/alibaba/higress/pkg/cmd/options"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type upgradeArgs struct {
|
||||
*InstallArgs
|
||||
}
|
||||
|
||||
func addUpgradeFlags(cmd *cobra.Command, args *upgradeArgs) {
|
||||
cmd.PersistentFlags().StringArrayVarP(&args.Set, "set", "s", nil, setFlagHelpStr)
|
||||
cmd.PersistentFlags().StringVarP(&args.ManifestsPath, "manifests", "d", "", manifestsFlagHelpStr)
|
||||
}
|
||||
|
||||
// newUpgradeCmd upgrades Istio control plane in-place with eligibility checks.
|
||||
func newUpgradeCmd() *cobra.Command {
|
||||
upgradeArgs := &upgradeArgs{
|
||||
InstallArgs: &InstallArgs{},
|
||||
}
|
||||
upgradeCmd := &cobra.Command{
|
||||
Use: "upgrade",
|
||||
Short: "Upgrade Higress in-place",
|
||||
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)
|
||||
},
|
||||
}
|
||||
addUpgradeFlags(upgradeCmd, upgradeArgs)
|
||||
flags := upgradeCmd.Flags()
|
||||
options.AddKubeConfigFlags(flags)
|
||||
return upgradeCmd
|
||||
}
|
||||
91
pkg/cmd/hgctl/util/filter.go
Normal file
91
pkg/cmd/hgctl/util/filter.go
Normal file
@@ -0,0 +1,91 @@
|
||||
// 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 (
|
||||
"bufio"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/google/yamlfmt/formatters/basic"
|
||||
)
|
||||
|
||||
var (
|
||||
formatterConfig = func() *basic.Config {
|
||||
cfg := basic.DefaultConfig()
|
||||
return cfg
|
||||
}()
|
||||
formatter = &basic.BasicFormatter{
|
||||
Config: formatterConfig,
|
||||
Features: basic.ConfigureFeaturesFromConfig(formatterConfig),
|
||||
}
|
||||
)
|
||||
|
||||
// FilterFunc is used to filter some contents of manifest.
|
||||
type FilterFunc func(string) string
|
||||
|
||||
func ApplyFilters(input string, filters ...FilterFunc) string {
|
||||
for _, filter := range filters {
|
||||
input = filter(input)
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
// LicenseFilter assumes that license is at the beginning.
|
||||
// So we just remove all the leading comments until the first non-comment line appears.
|
||||
func LicenseFilter(input string) string {
|
||||
var index int
|
||||
buf := bufio.NewReader(strings.NewReader(input))
|
||||
for {
|
||||
line, err := buf.ReadString('\n')
|
||||
if !strings.HasPrefix(line, "#") {
|
||||
return input[index:]
|
||||
}
|
||||
index += len(line)
|
||||
if err == io.EOF {
|
||||
return input[index:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SpaceFilter removes all leading and trailing space.
|
||||
func SpaceFilter(input string) string {
|
||||
return strings.TrimSpace(input)
|
||||
}
|
||||
|
||||
// SpaceLineFilter removes all space lines.
|
||||
func SpaceLineFilter(input string) string {
|
||||
var builder strings.Builder
|
||||
scanner := bufio.NewScanner(strings.NewReader(input))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
builder.WriteString(line)
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// FormatterFilter uses github.com/google/yamlfmt to format yaml file
|
||||
func FormatterFilter(input string) string {
|
||||
resBytes, err := formatter.Format([]byte(input))
|
||||
// todo: think about log
|
||||
if err != nil {
|
||||
return input
|
||||
}
|
||||
return string(resBytes)
|
||||
}
|
||||
98
pkg/cmd/hgctl/util/filter_test.go
Normal file
98
pkg/cmd/hgctl/util/filter_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// 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 (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLicenseFilter(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
input: `# license line
|
||||
content line`,
|
||||
want: `content line`,
|
||||
},
|
||||
{
|
||||
input: `# license line`,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
input: `# license line
|
||||
content line
|
||||
# comment line`,
|
||||
want: `content line
|
||||
# comment line`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
res := LicenseFilter(test.input)
|
||||
if res != test.want {
|
||||
t.Errorf("want %s\n but got %s", test.want, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpaceFilter(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
input: `
|
||||
content line
|
||||
`,
|
||||
want: "content line",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
res := SpaceFilter(test.input)
|
||||
if res != test.want {
|
||||
t.Errorf("want %s\n but got %s", test.want, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatterFilter(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
input: `key1: val1 `,
|
||||
want: `key1: val1
|
||||
`,
|
||||
},
|
||||
{
|
||||
input: `key1:
|
||||
key2: val2`,
|
||||
want: `key1:
|
||||
key2: val2
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
res := FormatterFilter(test.input)
|
||||
if res != test.want {
|
||||
t.Errorf("want \n%s\n but got \n%s\n", test.want, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
209
pkg/cmd/hgctl/util/path.go
Normal file
209
pkg/cmd/hgctl/util/path.go
Normal file
@@ -0,0 +1,209 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// PathSeparator is the separator between path elements.
|
||||
PathSeparator = "."
|
||||
// KVSeparator is the separator between the key and value in a key/value path element,
|
||||
KVSeparator = string(kvSeparatorRune)
|
||||
kvSeparatorRune = ':'
|
||||
|
||||
// InsertIndex is the index that means "insert" when setting values
|
||||
InsertIndex = -1
|
||||
|
||||
// PathSeparatorRune is the separator between path elements, as a rune.
|
||||
pathSeparatorRune = '.'
|
||||
// EscapedPathSeparator is what to use when the path shouldn't separate
|
||||
EscapedPathSeparator = "\\" + PathSeparator
|
||||
)
|
||||
|
||||
// ValidKeyRegex is a regex for a valid path key element.
|
||||
var ValidKeyRegex = regexp.MustCompile("^[a-zA-Z0-9_-]*$")
|
||||
|
||||
// Path is a path in slice form.
|
||||
type Path []string
|
||||
|
||||
// PathFromString converts a string path of form a.b.c to a string slice representation.
|
||||
func PathFromString(path string) Path {
|
||||
path = filepath.Clean(path)
|
||||
path = strings.TrimPrefix(path, PathSeparator)
|
||||
path = strings.TrimSuffix(path, PathSeparator)
|
||||
pv := splitEscaped(path, pathSeparatorRune)
|
||||
var r []string
|
||||
for _, str := range pv {
|
||||
if str != "" {
|
||||
str = strings.ReplaceAll(str, EscapedPathSeparator, PathSeparator)
|
||||
// Is str of the form node[expr], convert to "node", "[expr]"?
|
||||
nBracket := strings.IndexRune(str, '[')
|
||||
if nBracket > 0 {
|
||||
r = append(r, str[:nBracket], str[nBracket:])
|
||||
} else {
|
||||
// str is "[expr]" or "node"
|
||||
r = append(r, str)
|
||||
}
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// String converts a string slice path representation of form ["a", "b", "c"] to a string representation like "a.b.c".
|
||||
func (p Path) String() string {
|
||||
return strings.Join(p, PathSeparator)
|
||||
}
|
||||
|
||||
func (p Path) Equals(p2 Path) bool {
|
||||
if len(p) != len(p2) {
|
||||
return false
|
||||
}
|
||||
for i, pp := range p {
|
||||
if pp != p2[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ToYAMLPath converts a path string to path such that the first letter of each path element is lower case.
|
||||
func ToYAMLPath(path string) Path {
|
||||
p := PathFromString(path)
|
||||
for i := range p {
|
||||
p[i] = firstCharToLowerCase(p[i])
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// ToYAMLPathString converts a path string such that the first letter of each path element is lower case.
|
||||
func ToYAMLPathString(path string) string {
|
||||
return ToYAMLPath(path).String()
|
||||
}
|
||||
|
||||
// IsValidPathElement reports whether pe is a valid path element.
|
||||
func IsValidPathElement(pe string) bool {
|
||||
return ValidKeyRegex.MatchString(pe)
|
||||
}
|
||||
|
||||
// IsKVPathElement report whether pe is a key/value path element.
|
||||
func IsKVPathElement(pe string) bool {
|
||||
pe, ok := RemoveBrackets(pe)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
kv := splitEscaped(pe, kvSeparatorRune)
|
||||
if len(kv) != 2 || len(kv[0]) == 0 || len(kv[1]) == 0 {
|
||||
return false
|
||||
}
|
||||
return IsValidPathElement(kv[0])
|
||||
}
|
||||
|
||||
// IsVPathElement report whether pe is a value path element.
|
||||
func IsVPathElement(pe string) bool {
|
||||
pe, ok := RemoveBrackets(pe)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return len(pe) > 1 && pe[0] == ':'
|
||||
}
|
||||
|
||||
// IsNPathElement report whether pe is an index path element.
|
||||
func IsNPathElement(pe string) bool {
|
||||
pe, ok := RemoveBrackets(pe)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
n, err := strconv.Atoi(pe)
|
||||
return err == nil && n >= InsertIndex
|
||||
}
|
||||
|
||||
// PathKV returns the key and value string parts of the entire key/value path element.
|
||||
// It returns an error if pe is not a key/value path element.
|
||||
func PathKV(pe string) (k, v string, err error) {
|
||||
if !IsKVPathElement(pe) {
|
||||
return "", "", fmt.Errorf("%s is not a valid key:value path element", pe)
|
||||
}
|
||||
pe, _ = RemoveBrackets(pe)
|
||||
kv := splitEscaped(pe, kvSeparatorRune)
|
||||
return kv[0], kv[1], nil
|
||||
}
|
||||
|
||||
// PathV returns the value string part of the entire value path element.
|
||||
// It returns an error if pe is not a value path element.
|
||||
func PathV(pe string) (string, error) {
|
||||
// For :val, return the value only
|
||||
if IsVPathElement(pe) {
|
||||
v, _ := RemoveBrackets(pe)
|
||||
return v[1:], nil
|
||||
}
|
||||
|
||||
// For key:val, return the whole thing
|
||||
v, _ := RemoveBrackets(pe)
|
||||
if len(v) > 0 {
|
||||
return v, nil
|
||||
}
|
||||
return "", fmt.Errorf("%s is not a valid value path element", pe)
|
||||
}
|
||||
|
||||
// PathN returns the index part of the entire value path element.
|
||||
// It returns an error if pe is not an index path element.
|
||||
func PathN(pe string) (int, error) {
|
||||
if !IsNPathElement(pe) {
|
||||
return -1, fmt.Errorf("%s is not a valid index path element", pe)
|
||||
}
|
||||
v, _ := RemoveBrackets(pe)
|
||||
return strconv.Atoi(v)
|
||||
}
|
||||
|
||||
// RemoveBrackets removes the [] around pe and returns the resulting string. It returns false if pe is not surrounded
|
||||
// by [].
|
||||
func RemoveBrackets(pe string) (string, bool) {
|
||||
if !strings.HasPrefix(pe, "[") || !strings.HasSuffix(pe, "]") {
|
||||
return "", false
|
||||
}
|
||||
return pe[1 : len(pe)-1], true
|
||||
}
|
||||
|
||||
// splitEscaped splits a string using the rune r as a separator. It does not split on r if it's prefixed by \.
|
||||
func splitEscaped(s string, r rune) []string {
|
||||
var prev rune
|
||||
if len(s) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
prevIdx := 0
|
||||
var out []string
|
||||
for i, c := range s {
|
||||
if c == r && (i == 0 || (i > 0 && prev != '\\')) {
|
||||
out = append(out, s[prevIdx:i])
|
||||
prevIdx = i + 1
|
||||
}
|
||||
prev = c
|
||||
}
|
||||
out = append(out, s[prevIdx:])
|
||||
return out
|
||||
}
|
||||
|
||||
func firstCharToLowerCase(s string) string {
|
||||
return strings.ToLower(s[0:1]) + s[1:]
|
||||
}
|
||||
383
pkg/cmd/hgctl/util/path_test.go
Normal file
383
pkg/cmd/hgctl/util/path_test.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 util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSplitEscaped(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
desc: "empty",
|
||||
in: "",
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
desc: "no match",
|
||||
in: "foo",
|
||||
want: []string{"foo"},
|
||||
},
|
||||
{
|
||||
desc: "first",
|
||||
in: ":foo",
|
||||
want: []string{"", "foo"},
|
||||
},
|
||||
{
|
||||
desc: "last",
|
||||
in: "foo:",
|
||||
want: []string{"foo", ""},
|
||||
},
|
||||
{
|
||||
desc: "multiple",
|
||||
in: "foo:bar:baz",
|
||||
want: []string{"foo", "bar", "baz"},
|
||||
},
|
||||
{
|
||||
desc: "multiple with escapes",
|
||||
in: `foo\:bar:baz\:qux`,
|
||||
want: []string{`foo\:bar`, `baz\:qux`},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got, want := splitEscaped(tt.in, kvSeparatorRune), tt.want; !stringSlicesEqual(got, want) {
|
||||
t.Errorf("%s: got:%v, want:%v", tt.desc, got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsNPathElement(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in string
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
desc: "empty",
|
||||
in: "",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
desc: "negative",
|
||||
in: "[-45]",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
desc: "negative-1",
|
||||
in: "[-1]",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "valid",
|
||||
in: "[0]",
|
||||
expect: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got := IsNPathElement(tt.in); got != tt.expect {
|
||||
t.Errorf("%s: expect %v got %v", tt.desc, tt.expect, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func stringSlicesEqual(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i, aa := range a {
|
||||
if aa != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestPathFromString(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in string
|
||||
expect Path
|
||||
}{
|
||||
{
|
||||
desc: "no-path",
|
||||
in: "",
|
||||
expect: Path{},
|
||||
},
|
||||
{
|
||||
desc: "valid-path",
|
||||
in: "a.b.c",
|
||||
expect: Path{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
desc: "surround-periods",
|
||||
in: ".a.",
|
||||
expect: Path{"a"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got := PathFromString(tt.in); !got.Equals(tt.expect) {
|
||||
t.Errorf("%s: expect %v got %v", tt.desc, tt.expect, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToYAMLPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in string
|
||||
expect Path
|
||||
}{
|
||||
{
|
||||
desc: "all-uppercase",
|
||||
in: "A.B.C.D",
|
||||
expect: Path{"a", "b", "c", "d"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got := ToYAMLPath(tt.in); !got.Equals(tt.expect) {
|
||||
t.Errorf("%s: expect %v got %v", tt.desc, tt.expect, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsKVPathElement(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in string
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
desc: "valid",
|
||||
in: "[1:2]",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid",
|
||||
in: "[:2]",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
desc: "invalid-2",
|
||||
in: "[1:]",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
desc: "empty",
|
||||
in: "",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
desc: "no-brackets",
|
||||
in: "1:2",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
desc: "one-bracket",
|
||||
in: "[1:2",
|
||||
expect: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got := IsKVPathElement(tt.in); got != tt.expect {
|
||||
t.Errorf("%s: expect %v got %v", tt.desc, tt.expect, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsVPathElement(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in string
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
desc: "valid",
|
||||
in: "[:1]",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "kv-path-elem",
|
||||
in: "[1:2]",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
desc: "invalid",
|
||||
in: "1:2",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
desc: "empty",
|
||||
in: "",
|
||||
expect: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got := IsVPathElement(tt.in); got != tt.expect {
|
||||
t.Errorf("%s: expect %v got %v", tt.desc, tt.expect, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathKV(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in string
|
||||
wantK string
|
||||
wantV string
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
desc: "valid",
|
||||
in: "[1:2]",
|
||||
wantK: "1",
|
||||
wantV: "2",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "invalid",
|
||||
in: "[1:",
|
||||
wantErr: errors.New(""),
|
||||
},
|
||||
{
|
||||
desc: "empty",
|
||||
in: "",
|
||||
wantErr: errors.New(""),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if k, v, err := PathKV(tt.in); k != tt.wantK || v != tt.wantV || errNilCheck(err, tt.wantErr) {
|
||||
t.Errorf("%s: expect %v %v %v got %v %v %v", tt.desc, tt.wantK, tt.wantV, tt.wantErr, k, v, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathV(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in string
|
||||
want string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid-kv",
|
||||
in: "[1:2]",
|
||||
want: "1:2",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "valid-v",
|
||||
in: "[:1]",
|
||||
want: "1",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "invalid",
|
||||
in: "083fj",
|
||||
want: "",
|
||||
err: errors.New(""),
|
||||
},
|
||||
{
|
||||
desc: "empty",
|
||||
in: "",
|
||||
want: "",
|
||||
err: errors.New(""),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got, err := PathV(tt.in); got != tt.want || errNilCheck(err, tt.err) {
|
||||
t.Errorf("%s: expect %v %v got %v %v", tt.desc, tt.want, tt.err, got, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveBrackets(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in string
|
||||
expect string
|
||||
expectStat bool
|
||||
}{
|
||||
{
|
||||
desc: "has-brackets",
|
||||
in: "[yo]",
|
||||
expect: "yo",
|
||||
expectStat: true,
|
||||
},
|
||||
{
|
||||
desc: "one-bracket",
|
||||
in: "[yo",
|
||||
expect: "",
|
||||
expectStat: false,
|
||||
},
|
||||
{
|
||||
desc: "other-bracket",
|
||||
in: "yo]",
|
||||
expect: "",
|
||||
expectStat: false,
|
||||
},
|
||||
{
|
||||
desc: "no-brackets",
|
||||
in: "yo",
|
||||
expect: "",
|
||||
expectStat: false,
|
||||
},
|
||||
{
|
||||
desc: "empty",
|
||||
in: "",
|
||||
expect: "",
|
||||
expectStat: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got, stat := RemoveBrackets(tt.in); got != tt.expect || stat != tt.expectStat {
|
||||
t.Errorf("%s: expect %v %v got %v %v", tt.desc, tt.expect, tt.expectStat, got, stat)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func errNilCheck(err1, err2 error) bool {
|
||||
return (err1 == nil && err2 != nil) || (err1 != nil && err2 == nil)
|
||||
}
|
||||
311
pkg/cmd/hgctl/util/reflect.go
Normal file
311
pkg/cmd/hgctl/util/reflect.go
Normal file
@@ -0,0 +1,311 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// kindOf returns the reflection Kind that represents the dynamic type of value.
|
||||
// If value is a nil interface value, kindOf returns reflect.Invalid.
|
||||
func kindOf(value any) reflect.Kind {
|
||||
if value == nil {
|
||||
return reflect.Invalid
|
||||
}
|
||||
return reflect.TypeOf(value).Kind()
|
||||
}
|
||||
|
||||
// IsString reports whether value is a string type.
|
||||
func IsString(value any) bool {
|
||||
return kindOf(value) == reflect.String
|
||||
}
|
||||
|
||||
// IsPtr reports whether value is a ptr type.
|
||||
func IsPtr(value any) bool {
|
||||
return kindOf(value) == reflect.Ptr
|
||||
}
|
||||
|
||||
// IsMap reports whether value is a map type.
|
||||
func IsMap(value any) bool {
|
||||
return kindOf(value) == reflect.Map
|
||||
}
|
||||
|
||||
// IsMapPtr reports whether v is a map ptr type.
|
||||
func IsMapPtr(v any) bool {
|
||||
t := reflect.TypeOf(v)
|
||||
return t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Map
|
||||
}
|
||||
|
||||
// IsSlice reports whether value is a slice type.
|
||||
func IsSlice(value any) bool {
|
||||
return kindOf(value) == reflect.Slice
|
||||
}
|
||||
|
||||
// IsStruct reports whether value is a struct type
|
||||
func IsStruct(value any) bool {
|
||||
return kindOf(value) == reflect.Struct
|
||||
}
|
||||
|
||||
// IsSlicePtr reports whether v is a slice ptr type.
|
||||
func IsSlicePtr(v any) bool {
|
||||
return kindOf(v) == reflect.Ptr && reflect.TypeOf(v).Elem().Kind() == reflect.Slice
|
||||
}
|
||||
|
||||
// IsSliceInterfacePtr reports whether v is a slice ptr type.
|
||||
func IsSliceInterfacePtr(v any) bool {
|
||||
// Must use ValueOf because Elem().Elem() type resolves dynamically.
|
||||
vv := reflect.ValueOf(v)
|
||||
return vv.Kind() == reflect.Ptr && vv.Elem().Kind() == reflect.Interface && vv.Elem().Elem().Kind() == reflect.Slice
|
||||
}
|
||||
|
||||
// IsTypeStructPtr reports whether v is a struct ptr type.
|
||||
func IsTypeStructPtr(t reflect.Type) bool {
|
||||
if t == reflect.TypeOf(nil) {
|
||||
return false
|
||||
}
|
||||
return t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct
|
||||
}
|
||||
|
||||
// IsTypeSlicePtr reports whether v is a slice ptr type.
|
||||
func IsTypeSlicePtr(t reflect.Type) bool {
|
||||
if t == reflect.TypeOf(nil) {
|
||||
return false
|
||||
}
|
||||
return t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Slice
|
||||
}
|
||||
|
||||
// IsTypeMap reports whether v is a map type.
|
||||
func IsTypeMap(t reflect.Type) bool {
|
||||
if t == reflect.TypeOf(nil) {
|
||||
return false
|
||||
}
|
||||
return t.Kind() == reflect.Map
|
||||
}
|
||||
|
||||
// IsTypeInterface reports whether v is an interface.
|
||||
func IsTypeInterface(t reflect.Type) bool {
|
||||
if t == reflect.TypeOf(nil) {
|
||||
return false
|
||||
}
|
||||
return t.Kind() == reflect.Interface
|
||||
}
|
||||
|
||||
// IsTypeSliceOfInterface reports whether v is a slice of interface.
|
||||
func IsTypeSliceOfInterface(t reflect.Type) bool {
|
||||
if t == reflect.TypeOf(nil) {
|
||||
return false
|
||||
}
|
||||
return t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Interface
|
||||
}
|
||||
|
||||
// IsNilOrInvalidValue reports whether v is nil or reflect.Zero.
|
||||
func IsNilOrInvalidValue(v reflect.Value) bool {
|
||||
return !v.IsValid() || (v.Kind() == reflect.Ptr && v.IsNil()) || IsValueNil(v.Interface())
|
||||
}
|
||||
|
||||
// IsValueNil returns true if either value is nil, or has dynamic type {ptr,
|
||||
// map, slice} with value nil.
|
||||
func IsValueNil(value any) bool {
|
||||
if value == nil {
|
||||
return true
|
||||
}
|
||||
switch kindOf(value) {
|
||||
case reflect.Slice, reflect.Ptr, reflect.Map:
|
||||
return reflect.ValueOf(value).IsNil()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsValueNilOrDefault returns true if either IsValueNil(value) or the default
|
||||
// value for the type.
|
||||
func IsValueNilOrDefault(value any) bool {
|
||||
if IsValueNil(value) {
|
||||
return true
|
||||
}
|
||||
if !IsValueScalar(reflect.ValueOf(value)) {
|
||||
// Default value is nil for non-scalar types.
|
||||
return false
|
||||
}
|
||||
return value == reflect.New(reflect.TypeOf(value)).Elem().Interface()
|
||||
}
|
||||
|
||||
// IsValuePtr reports whether v is a ptr type.
|
||||
func IsValuePtr(v reflect.Value) bool {
|
||||
return v.Kind() == reflect.Ptr
|
||||
}
|
||||
|
||||
// IsValueInterface reports whether v is an interface type.
|
||||
func IsValueInterface(v reflect.Value) bool {
|
||||
return v.Kind() == reflect.Interface
|
||||
}
|
||||
|
||||
// IsValueStruct reports whether v is a struct type.
|
||||
func IsValueStruct(v reflect.Value) bool {
|
||||
return v.Kind() == reflect.Struct
|
||||
}
|
||||
|
||||
// IsValueStructPtr reports whether v is a struct ptr type.
|
||||
func IsValueStructPtr(v reflect.Value) bool {
|
||||
return v.Kind() == reflect.Ptr && IsValueStruct(v.Elem())
|
||||
}
|
||||
|
||||
// IsValueMap reports whether v is a map type.
|
||||
func IsValueMap(v reflect.Value) bool {
|
||||
return v.Kind() == reflect.Map
|
||||
}
|
||||
|
||||
// IsValueSlice reports whether v is a slice type.
|
||||
func IsValueSlice(v reflect.Value) bool {
|
||||
return v.Kind() == reflect.Slice
|
||||
}
|
||||
|
||||
// IsValueScalar reports whether v is a scalar type.
|
||||
func IsValueScalar(v reflect.Value) bool {
|
||||
if IsNilOrInvalidValue(v) {
|
||||
return false
|
||||
}
|
||||
if IsValuePtr(v) {
|
||||
if v.IsNil() {
|
||||
return false
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
return !IsValueStruct(v) && !IsValueMap(v) && !IsValueSlice(v)
|
||||
}
|
||||
|
||||
// ValuesAreSameType returns true if v1 and v2 has the same reflect.Type,
|
||||
// otherwise it returns false.
|
||||
func ValuesAreSameType(v1 reflect.Value, v2 reflect.Value) bool {
|
||||
return v1.Type() == v2.Type()
|
||||
}
|
||||
|
||||
// IsEmptyString returns true if value is an empty string.
|
||||
func IsEmptyString(value any) bool {
|
||||
if value == nil {
|
||||
return true
|
||||
}
|
||||
switch kindOf(value) {
|
||||
case reflect.String:
|
||||
if _, ok := value.(string); ok {
|
||||
return value.(string) == ""
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DeleteFromSlicePtr deletes an entry at index from the parent, which must be a slice ptr.
|
||||
func DeleteFromSlicePtr(parentSlice any, index int) error {
|
||||
pv := reflect.ValueOf(parentSlice)
|
||||
|
||||
if !IsSliceInterfacePtr(parentSlice) {
|
||||
return fmt.Errorf("deleteFromSlicePtr parent type is %T, must be *[]interface{}", parentSlice)
|
||||
}
|
||||
|
||||
pvv := pv.Elem()
|
||||
if pvv.Kind() == reflect.Interface {
|
||||
pvv = pvv.Elem()
|
||||
}
|
||||
|
||||
pv.Elem().Set(reflect.AppendSlice(pvv.Slice(0, index), pvv.Slice(index+1, pvv.Len())))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateSlicePtr updates an entry at index in the parent, which must be a slice ptr, with the given value.
|
||||
func UpdateSlicePtr(parentSlice any, index int, value any) error {
|
||||
pv := reflect.ValueOf(parentSlice)
|
||||
v := reflect.ValueOf(value)
|
||||
|
||||
if !IsSliceInterfacePtr(parentSlice) {
|
||||
return fmt.Errorf("updateSlicePtr parent type is %T, must be *[]interface{}", parentSlice)
|
||||
}
|
||||
|
||||
pvv := pv.Elem()
|
||||
if pvv.Kind() == reflect.Interface {
|
||||
pv.Elem().Elem().Index(index).Set(v)
|
||||
return nil
|
||||
}
|
||||
pv.Elem().Index(index).Set(v)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertIntoMap inserts value with key into parent which must be a map, map ptr, or interface to map.
|
||||
func InsertIntoMap(parentMap any, key any, value any) error {
|
||||
v := reflect.ValueOf(parentMap)
|
||||
kv := reflect.ValueOf(key)
|
||||
vv := reflect.ValueOf(value)
|
||||
|
||||
if v.Type().Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
if v.Type().Kind() == reflect.Interface {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if v.Type().Kind() != reflect.Map {
|
||||
return fmt.Errorf("insertIntoMap parent type is %T, must be map", parentMap)
|
||||
}
|
||||
|
||||
v.SetMapIndex(kv, vv)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteFromMap deletes an entry with the given key parent, which must be a map.
|
||||
func DeleteFromMap(parentMap any, key any) error {
|
||||
pv := reflect.ValueOf(parentMap)
|
||||
|
||||
if !IsMap(parentMap) {
|
||||
return fmt.Errorf("deleteFromMap parent type is %T, must be map", parentMap)
|
||||
}
|
||||
pv.SetMapIndex(reflect.ValueOf(key), reflect.Value{})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToIntValue returns 0, false if val is not a number type, otherwise it returns the int value of val.
|
||||
func ToIntValue(val any) (int, bool) {
|
||||
if IsValueNil(val) {
|
||||
return 0, false
|
||||
}
|
||||
v := reflect.ValueOf(val)
|
||||
switch {
|
||||
case IsIntKind(v.Kind()):
|
||||
return int(v.Int()), true
|
||||
case IsUintKind(v.Kind()):
|
||||
return int(v.Uint()), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// IsIntKind reports whether k is an integer kind of any size.
|
||||
func IsIntKind(k reflect.Kind) bool {
|
||||
switch k {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsUintKind reports whether k is an unsigned integer kind of any size.
|
||||
func IsUintKind(k reflect.Kind) bool {
|
||||
switch k {
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
78
pkg/cmd/hgctl/util/util.go
Normal file
78
pkg/cmd/hgctl/util/util.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 util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StripPrefix removes the given prefix from prefix.
|
||||
func StripPrefix(path, prefix string) string {
|
||||
pl := len(strings.Split(prefix, "/"))
|
||||
pv := strings.Split(path, "/")
|
||||
return strings.Join(pv[pl:], "/")
|
||||
}
|
||||
|
||||
func SplitSetFlag(flag string) (string, string) {
|
||||
items := strings.Split(flag, "=")
|
||||
if len(items) != 2 {
|
||||
return flag, ""
|
||||
}
|
||||
return strings.TrimSpace(items[0]), strings.TrimSpace(items[1])
|
||||
}
|
||||
|
||||
// IsFilePath reports whether the given URL is a local file path.
|
||||
func IsFilePath(path string) bool {
|
||||
return strings.Contains(path, "/") || strings.Contains(path, ".")
|
||||
}
|
||||
|
||||
// IsHTTPURL checks whether the given URL is a HTTP URL.
|
||||
func IsHTTPURL(path string) (bool, error) {
|
||||
u, err := url.Parse(path)
|
||||
valid := err == nil && u.Host != "" && (u.Scheme == "http" || u.Scheme == "https")
|
||||
if strings.HasPrefix(path, "http") && !valid {
|
||||
return false, fmt.Errorf("%s starts with http but is not a valid URL: %s", path, err)
|
||||
}
|
||||
return valid, nil
|
||||
}
|
||||
|
||||
// StringBoolMapToSlice creates and returns a slice of all the map keys with true.
|
||||
func StringBoolMapToSlice(m map[string]bool) []string {
|
||||
s := make([]string, 0, len(m))
|
||||
for k, v := range m {
|
||||
if v {
|
||||
s = append(s, k)
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// ParseValue parses string into a value
|
||||
func ParseValue(valueStr string) any {
|
||||
var value any
|
||||
if v, err := strconv.Atoi(valueStr); err == nil {
|
||||
value = v
|
||||
} else if v, err := strconv.ParseFloat(valueStr, 64); err == nil {
|
||||
value = v
|
||||
} else if v, err := strconv.ParseBool(valueStr); err == nil {
|
||||
value = v
|
||||
} else {
|
||||
value = strings.ReplaceAll(valueStr, "\\,", ",")
|
||||
}
|
||||
return value
|
||||
}
|
||||
318
pkg/cmd/hgctl/util/yaml.go
Normal file
318
pkg/cmd/hgctl/util/yaml.go
Normal file
@@ -0,0 +1,318 @@
|
||||
// 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 (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch/v5" // nolint: staticcheck
|
||||
"github.com/kylelemons/godebug/diff"
|
||||
yaml3 "k8s.io/apimachinery/pkg/util/yaml"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
//
|
||||
//func ToYAMLGeneric(root any) ([]byte, error) {
|
||||
// var vs []byte
|
||||
// if proto, ok := root.(proto.Message); ok {
|
||||
// v, err := protomarshal.ToYAML(proto)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// vs = []byte(v)
|
||||
// } else {
|
||||
// v, err := yaml.Marshal(root)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// vs = v
|
||||
// }
|
||||
// return vs, nil
|
||||
//}
|
||||
//
|
||||
//func MustToYAMLGeneric(root any) string {
|
||||
// var vs []byte
|
||||
// if proto, ok := root.(proto.Message); ok {
|
||||
// v, err := protomarshal.ToYAML(proto)
|
||||
// if err != nil {
|
||||
// return err.Error()
|
||||
// }
|
||||
// vs = []byte(v)
|
||||
// } else {
|
||||
// v, err := yaml.Marshal(root)
|
||||
// if err != nil {
|
||||
// return err.Error()
|
||||
// }
|
||||
// vs = v
|
||||
// }
|
||||
// return string(vs)
|
||||
//}
|
||||
|
||||
// ToYAML returns a YAML string representation of val, or the error string if an error occurs.
|
||||
func ToYAML(val any) string {
|
||||
y, err := yaml.Marshal(val)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return string(y)
|
||||
}
|
||||
|
||||
//
|
||||
//// ToYAMLWithJSONPB returns a YAML string representation of val (using jsonpb), or the error string if an error occurs.
|
||||
//func ToYAMLWithJSONPB(val proto.Message) string {
|
||||
// v := reflect.ValueOf(val)
|
||||
// if val == nil || (v.Kind() == reflect.Ptr && v.IsNil()) {
|
||||
// return "null"
|
||||
// }
|
||||
// js, err := protomarshal.ToJSONWithOptions(val, "", true)
|
||||
// if err != nil {
|
||||
// return err.Error()
|
||||
// }
|
||||
// yb, err := yaml.JSONToYAML([]byte(js))
|
||||
// if err != nil {
|
||||
// return err.Error()
|
||||
// }
|
||||
// return string(yb)
|
||||
//}
|
||||
//
|
||||
//// MarshalWithJSONPB returns a YAML string representation of val (using jsonpb).
|
||||
//func MarshalWithJSONPB(val proto.Message) (string, error) {
|
||||
// return protomarshal.ToYAML(val)
|
||||
//}
|
||||
//
|
||||
//// UnmarshalWithJSONPB unmarshals y into out using gogo jsonpb (required for many proto defined structs).
|
||||
//func UnmarshalWithJSONPB(y string, out proto.Message, allowUnknownField bool) error {
|
||||
// // Treat nothing as nothing. If we called jsonpb.Unmarshaler it would return the same.
|
||||
// if y == "" {
|
||||
// return nil
|
||||
// }
|
||||
// jb, err := yaml.YAMLToJSON([]byte(y))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// if allowUnknownField {
|
||||
// err = protomarshal.UnmarshalAllowUnknown(jb, out)
|
||||
// } else {
|
||||
// err = protomarshal.Unmarshal(jb, out)
|
||||
// }
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// return nil
|
||||
//}
|
||||
|
||||
// OverlayTrees performs a sequential JSON strategic of overlays over base.
|
||||
func OverlayTrees(base map[string]any, overlays ...map[string]any) (map[string]any, error) {
|
||||
needsOverlay := false
|
||||
for _, o := range overlays {
|
||||
if len(o) > 0 {
|
||||
needsOverlay = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !needsOverlay {
|
||||
// Avoid expensive overlay if possible
|
||||
return base, nil
|
||||
}
|
||||
bby, err := yaml.Marshal(base)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
by := string(bby)
|
||||
|
||||
for _, o := range overlays {
|
||||
oy, err := yaml.Marshal(o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
by, err = OverlayYAML(by, string(oy))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
out := make(map[string]any)
|
||||
err = yaml.Unmarshal([]byte(by), &out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// OverlayYAML patches the overlay tree over the base tree and returns the result. All trees are expressed as YAML
|
||||
// strings.
|
||||
func OverlayYAML(base, overlay string) (string, error) {
|
||||
if strings.TrimSpace(base) == "" {
|
||||
return overlay, nil
|
||||
}
|
||||
if strings.TrimSpace(overlay) == "" {
|
||||
return base, nil
|
||||
}
|
||||
bj, err := yaml.YAMLToJSON([]byte(base))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("yamlToJSON error in base: %s\n%s", err, bj)
|
||||
}
|
||||
oj, err := yaml.YAMLToJSON([]byte(overlay))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("yamlToJSON error in overlay: %s\n%s", err, oj)
|
||||
}
|
||||
if base == "" {
|
||||
bj = []byte("{}")
|
||||
}
|
||||
if overlay == "" {
|
||||
oj = []byte("{}")
|
||||
}
|
||||
|
||||
merged, err := jsonpatch.MergePatch(bj, oj)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("json merge error (%s) for base object: \n%s\n override object: \n%s", err, bj, oj)
|
||||
}
|
||||
my, err := yaml.JSONToYAML(merged)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("jsonToYAML error (%s) for merged object: \n%s", err, merged)
|
||||
}
|
||||
|
||||
return string(my), nil
|
||||
}
|
||||
|
||||
// yamlDiff compares single YAML file
|
||||
func yamlDiff(a, b string) string {
|
||||
ao, bo := make(map[string]any), make(map[string]any)
|
||||
if err := yaml.Unmarshal([]byte(a), &ao); err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
if err := yaml.Unmarshal([]byte(b), &bo); err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
ay, err := yaml.Marshal(ao)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
by, err := yaml.Marshal(bo)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
return diff.Diff(string(ay), string(by))
|
||||
}
|
||||
|
||||
// yamlStringsToList yaml string parse to string list
|
||||
func yamlStringsToList(str string) []string {
|
||||
reader := bufio.NewReader(strings.NewReader(str))
|
||||
decoder := yaml3.NewYAMLReader(reader)
|
||||
res := make([]string, 0)
|
||||
for {
|
||||
doc, err := decoder.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
chunk := bytes.TrimSpace(doc)
|
||||
res = append(res, string(chunk))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// multiYamlDiffOutput multi yaml diff output format
|
||||
func multiYamlDiffOutput(res, diff string) string {
|
||||
if res == "" {
|
||||
return diff
|
||||
}
|
||||
if diff == "" {
|
||||
return res
|
||||
}
|
||||
|
||||
return res + "\n" + diff
|
||||
}
|
||||
|
||||
func diffStringList(l1, l2 []string) string {
|
||||
var maxLen int
|
||||
var minLen int
|
||||
var l1Max bool
|
||||
res := ""
|
||||
if len(l1)-len(l2) > 0 {
|
||||
maxLen = len(l1)
|
||||
minLen = len(l2)
|
||||
l1Max = true
|
||||
} else {
|
||||
maxLen = len(l2)
|
||||
minLen = len(l1)
|
||||
l1Max = false
|
||||
}
|
||||
|
||||
for i := 0; i < maxLen; i++ {
|
||||
d := ""
|
||||
if i >= minLen {
|
||||
if l1Max {
|
||||
d = yamlDiff(l1[i], "")
|
||||
} else {
|
||||
d = yamlDiff("", l2[i])
|
||||
}
|
||||
} else {
|
||||
d = yamlDiff(l1[i], l2[i])
|
||||
}
|
||||
res = multiYamlDiffOutput(res, d)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// YAMLDiff compares multiple YAML files and single YAML file
|
||||
func YAMLDiff(a, b string) string {
|
||||
al := yamlStringsToList(a)
|
||||
bl := yamlStringsToList(b)
|
||||
res := diffStringList(al, bl)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// IsYAMLEqual reports whether the YAML in strings a and b are equal.
|
||||
func IsYAMLEqual(a, b string) bool {
|
||||
if strings.TrimSpace(a) == "" && strings.TrimSpace(b) == "" {
|
||||
return true
|
||||
}
|
||||
ajb, err := yaml.YAMLToJSON([]byte(a))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
bjb, err := yaml.YAMLToJSON([]byte(b))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return bytes.Equal(ajb, bjb)
|
||||
}
|
||||
|
||||
// IsYAMLEmpty reports whether the YAML string y is logically empty.
|
||||
func IsYAMLEmpty(y string) bool {
|
||||
var yc []string
|
||||
for _, l := range strings.Split(y, "\n") {
|
||||
yt := strings.TrimSpace(l)
|
||||
if !strings.HasPrefix(yt, "#") && !strings.HasPrefix(yt, "---") {
|
||||
yc = append(yc, l)
|
||||
}
|
||||
}
|
||||
res := strings.TrimSpace(strings.Join(yc, "\n"))
|
||||
return res == "{}" || res == ""
|
||||
}
|
||||
363
pkg/cmd/hgctl/util/yaml_test.go
Normal file
363
pkg/cmd/hgctl/util/yaml_test.go
Normal file
@@ -0,0 +1,363 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestToYAML(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
inVals any
|
||||
expectedOut string
|
||||
}{
|
||||
{
|
||||
desc: "valid-yaml",
|
||||
inVals: map[string]any{
|
||||
"foo": "bar",
|
||||
"yo": map[string]any{
|
||||
"istio": "bar",
|
||||
},
|
||||
},
|
||||
expectedOut: `foo: bar
|
||||
yo:
|
||||
istio: bar
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "alphabetical",
|
||||
inVals: map[string]any{
|
||||
"foo": "yaml",
|
||||
"abc": "f",
|
||||
},
|
||||
expectedOut: `abc: f
|
||||
foo: yaml
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "expected-err-nil",
|
||||
inVals: nil,
|
||||
expectedOut: "null\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got := ToYAML(tt.inVals); got != tt.expectedOut {
|
||||
t.Errorf("%s: expected out %v got %s", tt.desc, tt.expectedOut, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverlayTrees(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
inBase map[string]any
|
||||
inOverlays map[string]any
|
||||
expectedOverlay map[string]any
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
desc: "overlay-valid",
|
||||
inBase: map[string]any{
|
||||
"foo": "bar",
|
||||
"baz": "naz",
|
||||
},
|
||||
inOverlays: map[string]any{
|
||||
"foo": "laz",
|
||||
},
|
||||
expectedOverlay: map[string]any{
|
||||
"baz": "naz",
|
||||
"foo": "laz",
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "overlay-key-does-not-exist",
|
||||
inBase: map[string]any{
|
||||
"foo": "bar",
|
||||
"baz": "naz",
|
||||
},
|
||||
inOverlays: map[string]any{
|
||||
"i-dont-exist": "i-really-dont-exist",
|
||||
},
|
||||
expectedOverlay: map[string]any{
|
||||
"baz": "naz",
|
||||
"foo": "bar",
|
||||
"i-dont-exist": "i-really-dont-exist",
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "remove-key-val",
|
||||
inBase: map[string]any{
|
||||
"foo": "bar",
|
||||
},
|
||||
inOverlays: map[string]any{
|
||||
"foo": nil,
|
||||
},
|
||||
expectedOverlay: map[string]any{},
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if gotOverlays, err := OverlayTrees(tt.inBase, tt.inOverlays); !reflect.DeepEqual(gotOverlays, tt.expectedOverlay) ||
|
||||
((err != nil && tt.expectedErr == nil) || (err == nil && tt.expectedErr != nil)) {
|
||||
t.Errorf("%s: expected overlay & err %v %v got %v %v", tt.desc, tt.expectedOverlay, tt.expectedErr,
|
||||
gotOverlays, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverlayYAML(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
base string
|
||||
overlay string
|
||||
expect string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "overlay-yaml",
|
||||
base: `foo: bar
|
||||
yo: lo
|
||||
`,
|
||||
overlay: `yo: go`,
|
||||
expect: `foo: bar
|
||||
yo: go
|
||||
`,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "combine-yaml",
|
||||
base: `foo: bar`,
|
||||
overlay: `baz: razmatazz`,
|
||||
expect: `baz: razmatazz
|
||||
foo: bar
|
||||
`,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "blank",
|
||||
base: `R#)*J#FN`,
|
||||
overlay: `FM#)M#F(*#M`,
|
||||
expect: "",
|
||||
err: errors.New("invalid json"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got, err := OverlayYAML(tt.base, tt.overlay); got != tt.expect || ((tt.err != nil && err == nil) || (tt.err == nil && err != nil)) {
|
||||
t.Errorf("%s: expected overlay&err %v %v got %v %v", tt.desc, tt.expect, tt.err, got, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestYAMLDiff(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
diff1 string
|
||||
diff2 string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
desc: "1-line-diff",
|
||||
diff1: `hola: yo
|
||||
foo: bar
|
||||
goo: tar
|
||||
`,
|
||||
diff2: `hola: yo
|
||||
foo: bar
|
||||
notgoo: nottar
|
||||
`,
|
||||
expect: ` foo: bar
|
||||
-goo: tar
|
||||
hola: yo
|
||||
+notgoo: nottar
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "no-diff",
|
||||
diff1: `foo: bar`,
|
||||
diff2: `foo: bar`,
|
||||
expect: ``,
|
||||
},
|
||||
{
|
||||
desc: "invalid-yaml",
|
||||
diff1: `Ij#**#f#`,
|
||||
diff2: `fm*##)n`,
|
||||
expect: "error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type map[string]interface {}",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got := YAMLDiff(tt.diff1, tt.diff2); got != tt.expect {
|
||||
t.Errorf("%s: expect %v got %v", tt.desc, tt.expect, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleYAMLDiff(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
diff1 string
|
||||
diff2 string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
desc: "1-line-diff",
|
||||
diff1: `hola: yo
|
||||
foo: bar
|
||||
goo: tar
|
||||
---
|
||||
hola: yo1
|
||||
foo: bar1
|
||||
goo: tar1
|
||||
`,
|
||||
diff2: `hola: yo
|
||||
foo: bar
|
||||
notgoo: nottar
|
||||
`,
|
||||
expect: ` foo: bar
|
||||
-goo: tar
|
||||
hola: yo
|
||||
+notgoo: nottar
|
||||
|
||||
-foo: bar1
|
||||
-goo: tar1
|
||||
-hola: yo1
|
||||
+{}
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "no-diff",
|
||||
diff1: `foo: bar
|
||||
---
|
||||
foo: bar1
|
||||
`,
|
||||
diff2: `foo: bar
|
||||
---
|
||||
foo: bar1
|
||||
`,
|
||||
expect: ``,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got := YAMLDiff(tt.diff1, tt.diff2); got != tt.expect {
|
||||
t.Errorf("%s: expect %v got %v", tt.desc, tt.expect, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsYAMLEqual(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in1 string
|
||||
in2 string
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
desc: "yaml-equal",
|
||||
in1: `foo: bar`,
|
||||
in2: `foo: bar`,
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "bad-yaml-1",
|
||||
in1: "O#JF*()#",
|
||||
in2: `foo: bar`,
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
desc: "bad-yaml-2",
|
||||
in1: `foo: bar`,
|
||||
in2: "#OHJ*#()F",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
desc: "yaml-not-equal",
|
||||
in1: `zinc: iron
|
||||
stoichiometry: avagadro
|
||||
`,
|
||||
in2: `i-swear: i-am
|
||||
definitely-not: in1
|
||||
`,
|
||||
expect: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got := IsYAMLEqual(tt.in1, tt.in2); got != tt.expect {
|
||||
t.Errorf("%v: got %v want %v", tt.desc, got, tt.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsYAMLEmpty(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in string
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
desc: "completely-empty",
|
||||
in: "",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "comment-logically-empty",
|
||||
in: `# this is a comment
|
||||
# this is another comment that serves no purpose
|
||||
# (like all comments usually do)
|
||||
`,
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "start-yaml",
|
||||
in: `--- I dont mean anything`,
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "combine-comments-and-yaml",
|
||||
in: `#this is another comment
|
||||
foo: bar
|
||||
# ^ that serves purpose
|
||||
`,
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
desc: "yaml-not-empty",
|
||||
in: `foo: bar`,
|
||||
expect: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got := IsYAMLEmpty(tt.in); got != tt.expect {
|
||||
t.Errorf("%v: expect %v got %v", tt.desc, tt.expect, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -34,8 +34,6 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
yamlOutput = "yaml"
|
||||
jsonOutput = "json"
|
||||
higressCoreContainerName = "higress-core"
|
||||
higressGatewayContainerName = "higress-gateway"
|
||||
)
|
||||
|
||||
@@ -15,3 +15,7 @@
|
||||
package constants
|
||||
|
||||
const DefaultIngressClass = "higress"
|
||||
|
||||
const KnativeIngressCRDName = "ingresses.networking.internal.knative.dev"
|
||||
|
||||
const KnativeServicesCRDName = "services.serving.knative.dev"
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/gogo/protobuf/jsonpb"
|
||||
"github.com/gogo/protobuf/types"
|
||||
"github.com/golang/protobuf/ptypes/wrappers"
|
||||
"go.uber.org/atomic"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
extensions "istio.io/api/extensions/v1alpha1"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
@@ -109,7 +110,7 @@ type IngressConfig struct {
|
||||
|
||||
RegistryReconciler *reconcile.Reconciler
|
||||
|
||||
mcpbridgeReconciled bool
|
||||
mcpbridgeReconciled *atomic.Bool
|
||||
|
||||
mcpbridgeController mcpbridge.McpBridgeController
|
||||
|
||||
@@ -154,7 +155,7 @@ func NewIngressConfig(localKubeClient kube.Client, XDSUpdater model.XDSUpdater,
|
||||
common.CreateConvertedName(clusterId, "global"),
|
||||
watchedSecretSet: sets.NewSet(),
|
||||
namespace: namespace,
|
||||
mcpbridgeReconciled: true,
|
||||
mcpbridgeReconciled: atomic.NewBool(true),
|
||||
wasmPlugins: make(map[string]*extensions.WasmPlugin),
|
||||
http2rpcs: make(map[string]*higressv1.Http2Rpc),
|
||||
}
|
||||
@@ -947,9 +948,7 @@ func (m *IngressConfig) AddOrUpdateMcpBridge(clusterNamespacedName util.ClusterN
|
||||
clusterNamespacedName.Namespace, clusterNamespacedName.Name)
|
||||
return
|
||||
}
|
||||
m.mutex.Lock()
|
||||
m.mcpbridgeReconciled = false
|
||||
m.mutex.Unlock()
|
||||
m.mcpbridgeReconciled.Store(false)
|
||||
if m.RegistryReconciler == nil {
|
||||
m.RegistryReconciler = reconcile.NewReconciler(func() {
|
||||
metadata := config.Meta{
|
||||
@@ -966,12 +965,12 @@ func (m *IngressConfig) AddOrUpdateMcpBridge(clusterNamespacedName util.ClusterN
|
||||
}, m.localKubeClient, m.namespace)
|
||||
}
|
||||
reconciler := m.RegistryReconciler
|
||||
go func() {
|
||||
reconciler.Reconcile(mcpbridge)
|
||||
m.mutex.Lock()
|
||||
m.mcpbridgeReconciled = true
|
||||
m.mutex.Unlock()
|
||||
}()
|
||||
err = reconciler.Reconcile(mcpbridge)
|
||||
if err != nil {
|
||||
IngressLog.Errorf("Mcpbridge reconcile failed, err:%v", err)
|
||||
return
|
||||
}
|
||||
m.mcpbridgeReconciled.Store(true)
|
||||
}
|
||||
|
||||
func (m *IngressConfig) DeleteMcpBridge(clusterNamespacedName util.ClusterNamespacedName) {
|
||||
@@ -1405,7 +1404,7 @@ func (m *IngressConfig) HasSynced() bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if !m.mcpbridgeController.HasSynced() || !m.mcpbridgeReconciled {
|
||||
if !m.mcpbridgeController.HasSynced() || !m.mcpbridgeReconciled.Load() {
|
||||
return false
|
||||
}
|
||||
if !m.wasmPluginController.HasSynced() {
|
||||
|
||||
552
pkg/ingress/config/kingress_config.go
Normal file
552
pkg/ingress/config/kingress_config.go
Normal file
@@ -0,0 +1,552 @@
|
||||
// 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 (
|
||||
"sync"
|
||||
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pilot/pkg/util/sets"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/constants"
|
||||
"istio.io/istio/pkg/config/schema/collection"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
listersv1 "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/annotations"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/common"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/kingress"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/secret"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/util"
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
"github.com/alibaba/higress/pkg/kube"
|
||||
"github.com/alibaba/higress/registry/reconcile"
|
||||
)
|
||||
|
||||
var (
|
||||
_ model.ConfigStoreCache = &KIngressConfig{}
|
||||
_ model.IngressStore = &KIngressConfig{}
|
||||
)
|
||||
|
||||
type KIngressConfig struct {
|
||||
// key: cluster id
|
||||
remoteIngressControllers map[string]common.KIngressController
|
||||
mutex sync.RWMutex
|
||||
|
||||
ingressRouteCache model.IngressRouteCollection
|
||||
ingressDomainCache model.IngressDomainCollection
|
||||
|
||||
localKubeClient kube.Client
|
||||
virtualServiceHandlers []model.EventHandler
|
||||
gatewayHandlers []model.EventHandler
|
||||
envoyFilterHandlers []model.EventHandler
|
||||
WatchErrorHandler cache.WatchErrorHandler
|
||||
|
||||
cachedEnvoyFilters []config.Config
|
||||
|
||||
watchedSecretSet sets.Set
|
||||
|
||||
RegistryReconciler *reconcile.Reconciler
|
||||
|
||||
XDSUpdater model.XDSUpdater
|
||||
|
||||
annotationHandler annotations.AnnotationHandler
|
||||
|
||||
globalGatewayName string
|
||||
|
||||
namespace string
|
||||
|
||||
clusterId string
|
||||
}
|
||||
|
||||
func NewKIngressConfig(localKubeClient kube.Client, XDSUpdater model.XDSUpdater, namespace, clusterId string) *KIngressConfig {
|
||||
if localKubeClient.KIngressInformer() == nil {
|
||||
return nil
|
||||
}
|
||||
if clusterId == "Kubernetes" {
|
||||
clusterId = ""
|
||||
}
|
||||
config := &KIngressConfig{
|
||||
remoteIngressControllers: make(map[string]common.KIngressController),
|
||||
localKubeClient: localKubeClient,
|
||||
XDSUpdater: XDSUpdater,
|
||||
annotationHandler: annotations.NewAnnotationHandlerManager(),
|
||||
clusterId: clusterId,
|
||||
globalGatewayName: namespace + "/" +
|
||||
common.CreateConvertedName(clusterId, "global"),
|
||||
watchedSecretSet: sets.NewSet(),
|
||||
namespace: namespace,
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) RegisterEventHandler(kind config.GroupVersionKind, f model.EventHandler) {
|
||||
IngressLog.Infof("register resource %v", kind)
|
||||
switch kind {
|
||||
case gvk.VirtualService:
|
||||
m.virtualServiceHandlers = append(m.virtualServiceHandlers, f)
|
||||
|
||||
case gvk.Gateway:
|
||||
m.gatewayHandlers = append(m.gatewayHandlers, f)
|
||||
|
||||
case gvk.EnvoyFilter:
|
||||
m.envoyFilterHandlers = append(m.envoyFilterHandlers, f)
|
||||
}
|
||||
|
||||
for _, remoteIngressController := range m.remoteIngressControllers {
|
||||
remoteIngressController.RegisterEventHandler(kind, f)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) AddLocalCluster(options common.Options) common.KIngressController {
|
||||
secretController := secret.NewController(m.localKubeClient, options.ClusterId)
|
||||
secretController.AddEventHandler(m.ReflectSecretChanges)
|
||||
|
||||
var ingressController common.KIngressController
|
||||
|
||||
ingressController = kingress.NewController(m.localKubeClient, m.localKubeClient, options, secretController)
|
||||
|
||||
m.remoteIngressControllers[options.ClusterId] = ingressController
|
||||
return ingressController
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) InitializeCluster(ingressController common.KIngressController, stop <-chan struct{}) error {
|
||||
_ = ingressController.SetWatchErrorHandler(m.WatchErrorHandler)
|
||||
go ingressController.Run(stop)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) List(typ config.GroupVersionKind, namespace string) ([]config.Config, error) {
|
||||
if typ == gvk.EnvoyFilter || typ == gvk.DestinationRule || typ == gvk.WasmPlugin || typ == gvk.ServiceEntry {
|
||||
return nil, nil
|
||||
}
|
||||
if typ != gvk.Gateway && typ != gvk.VirtualService {
|
||||
return nil, common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
// Currently, only support list all namespaces gateways or virtualservices.
|
||||
if namespace != "" {
|
||||
IngressLog.Warnf("ingress store only support type %s of all namespace.", typ)
|
||||
return nil, common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
var configs []config.Config
|
||||
m.mutex.RLock()
|
||||
for _, ingressController := range m.remoteIngressControllers {
|
||||
configs = append(configs, ingressController.List()...)
|
||||
}
|
||||
m.mutex.RUnlock()
|
||||
|
||||
common.SortIngressByCreationTime(configs)
|
||||
wrapperConfigs := m.createWrapperConfigs(configs)
|
||||
|
||||
IngressLog.Infof("resource type %s, configs number %d", typ, len(wrapperConfigs))
|
||||
switch typ {
|
||||
case gvk.Gateway:
|
||||
return m.convertGateways(wrapperConfigs), nil
|
||||
case gvk.VirtualService:
|
||||
return m.convertVirtualService(wrapperConfigs), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) createWrapperConfigs(configs []config.Config) []common.WrapperConfig {
|
||||
var wrapperConfigs []common.WrapperConfig
|
||||
|
||||
// Init global context
|
||||
clusterSecretListers := map[string]listersv1.SecretLister{}
|
||||
clusterServiceListers := map[string]listersv1.ServiceLister{}
|
||||
m.mutex.RLock()
|
||||
for clusterId, controller := range m.remoteIngressControllers {
|
||||
clusterSecretListers[clusterId] = controller.SecretLister()
|
||||
clusterServiceListers[clusterId] = controller.ServiceLister()
|
||||
}
|
||||
m.mutex.RUnlock()
|
||||
globalContext := &annotations.GlobalContext{
|
||||
WatchedSecrets: sets.NewSet(),
|
||||
ClusterSecretLister: clusterSecretListers,
|
||||
ClusterServiceList: clusterServiceListers,
|
||||
}
|
||||
|
||||
for idx := range configs {
|
||||
rawConfig := configs[idx]
|
||||
annotationsConfig := &annotations.Ingress{
|
||||
Meta: annotations.Meta{
|
||||
Namespace: rawConfig.Namespace,
|
||||
Name: rawConfig.Name,
|
||||
RawClusterId: common.GetRawClusterId(rawConfig.Annotations),
|
||||
ClusterId: common.GetClusterId(rawConfig.Annotations),
|
||||
},
|
||||
}
|
||||
_ = m.annotationHandler.Parse(rawConfig.Annotations, annotationsConfig, globalContext)
|
||||
wrapperConfigs = append(wrapperConfigs, common.WrapperConfig{
|
||||
Config: &rawConfig,
|
||||
AnnotationsConfig: annotationsConfig,
|
||||
})
|
||||
}
|
||||
|
||||
m.mutex.Lock()
|
||||
m.watchedSecretSet = globalContext.WatchedSecrets
|
||||
m.mutex.Unlock()
|
||||
|
||||
return wrapperConfigs
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) convertGateways(configs []common.WrapperConfig) []config.Config {
|
||||
convertOptions := common.ConvertOptions{
|
||||
IngressDomainCache: common.NewIngressDomainCache(),
|
||||
Gateways: map[string]*common.WrapperGateway{},
|
||||
}
|
||||
|
||||
for idx := range configs {
|
||||
cfg := configs[idx]
|
||||
clusterId := common.GetClusterId(cfg.Config.Annotations)
|
||||
m.mutex.RLock()
|
||||
ingressController := m.remoteIngressControllers[clusterId]
|
||||
m.mutex.RUnlock()
|
||||
if ingressController == nil {
|
||||
continue
|
||||
}
|
||||
if err := ingressController.ConvertGateway(&convertOptions, &cfg); err != nil {
|
||||
IngressLog.Errorf("Convert ingress %s/%s to gateway fail in cluster %s, err %v", cfg.Config.Namespace, cfg.Config.Name, clusterId, err)
|
||||
}
|
||||
}
|
||||
|
||||
// apply annotation
|
||||
for _, wrapperGateway := range convertOptions.Gateways {
|
||||
m.annotationHandler.ApplyGateway(wrapperGateway.Gateway, wrapperGateway.WrapperConfig.AnnotationsConfig)
|
||||
}
|
||||
|
||||
m.mutex.Lock()
|
||||
m.ingressDomainCache = convertOptions.IngressDomainCache.Extract()
|
||||
m.mutex.Unlock()
|
||||
out := make([]config.Config, 0, len(convertOptions.Gateways))
|
||||
for _, gateway := range convertOptions.Gateways {
|
||||
cleanHost := common.CleanHost(gateway.Host)
|
||||
out = append(out, config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.Gateway,
|
||||
Name: common.CreateConvertedName(constants.IstioIngressGatewayName, cleanHost),
|
||||
Namespace: m.namespace,
|
||||
Annotations: map[string]string{
|
||||
common.ClusterIdAnnotation: gateway.ClusterId,
|
||||
common.HostAnnotation: gateway.Host,
|
||||
},
|
||||
},
|
||||
Spec: gateway.Gateway,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) convertVirtualService(configs []common.WrapperConfig) []config.Config {
|
||||
convertOptions := common.ConvertOptions{
|
||||
IngressRouteCache: common.NewIngressRouteCache(),
|
||||
VirtualServices: map[string]*common.WrapperVirtualService{},
|
||||
HTTPRoutes: map[string][]*common.WrapperHTTPRoute{},
|
||||
Route2Ingress: map[string]*common.WrapperConfigWithRuleKey{},
|
||||
}
|
||||
|
||||
// convert http route
|
||||
for idx := range configs {
|
||||
cfg := configs[idx]
|
||||
clusterId := common.GetClusterId(cfg.Config.Annotations)
|
||||
m.mutex.RLock()
|
||||
ingressController := m.remoteIngressControllers[clusterId]
|
||||
m.mutex.RUnlock()
|
||||
if ingressController == nil {
|
||||
continue
|
||||
}
|
||||
if err := ingressController.ConvertHTTPRoute(&convertOptions, &cfg); err != nil {
|
||||
IngressLog.Errorf("Convert ingress %s/%s to HTTP route fail in cluster %s, err %v", cfg.Config.Namespace, cfg.Config.Name, clusterId, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply annotation on routes
|
||||
for _, routes := range convertOptions.HTTPRoutes {
|
||||
for _, route := range routes {
|
||||
m.annotationHandler.ApplyRoute(route.HTTPRoute, route.WrapperConfig.AnnotationsConfig)
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize weighted cluster to make sure the sum of weight is 100.
|
||||
for _, host := range convertOptions.HTTPRoutes {
|
||||
for _, route := range host {
|
||||
normalizeWeightedKCluster(convertOptions.IngressRouteCache, route)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply annotation on virtual services Only IP-control and do nothing
|
||||
for _, virtualService := range convertOptions.VirtualServices {
|
||||
m.annotationHandler.ApplyVirtualServiceHandler(virtualService.VirtualService, virtualService.WrapperConfig.AnnotationsConfig)
|
||||
}
|
||||
|
||||
// Apply app root for per host.
|
||||
m.applyAppRoot(&convertOptions)
|
||||
|
||||
// Apply internal active redirect for error page.
|
||||
m.applyInternalActiveRedirect(&convertOptions)
|
||||
|
||||
m.mutex.Lock()
|
||||
m.ingressRouteCache = convertOptions.IngressRouteCache.Extract()
|
||||
m.mutex.Unlock()
|
||||
|
||||
// Convert http route to virtual service
|
||||
out := make([]config.Config, 0, len(convertOptions.HTTPRoutes))
|
||||
for host, routes := range convertOptions.HTTPRoutes {
|
||||
if len(routes) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
cleanHost := common.CleanHost(host)
|
||||
// namespace/name, name format: (istio cluster id)-host
|
||||
gateways := []string{m.namespace + "/" +
|
||||
common.CreateConvertedName(m.clusterId, cleanHost),
|
||||
common.CreateConvertedName(constants.IstioIngressGatewayName, cleanHost)}
|
||||
if host != "*" {
|
||||
gateways = append(gateways, m.globalGatewayName)
|
||||
}
|
||||
|
||||
wrapperVS, exist := convertOptions.VirtualServices[host]
|
||||
if !exist {
|
||||
IngressLog.Warnf("virtual service for host %s does not exist.", host)
|
||||
}
|
||||
vs := wrapperVS.VirtualService
|
||||
vs.Gateways = gateways
|
||||
|
||||
for _, route := range routes {
|
||||
vs.Http = append(vs.Http, route.HTTPRoute)
|
||||
}
|
||||
|
||||
firstRoute := routes[0]
|
||||
out = append(out, config.Config{
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.VirtualService,
|
||||
Name: common.CreateConvertedName(constants.IstioIngressGatewayName, firstRoute.WrapperConfig.Config.Namespace, firstRoute.WrapperConfig.Config.Name, cleanHost),
|
||||
Namespace: m.namespace,
|
||||
Annotations: map[string]string{
|
||||
common.ClusterIdAnnotation: firstRoute.ClusterId,
|
||||
},
|
||||
},
|
||||
Spec: vs,
|
||||
})
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// Make sure that the sum of traffic split ratio is 100, if it is not 100, it will be normalized
|
||||
func normalizeWeightedKCluster(cache *common.IngressRouteCache, route *common.WrapperHTTPRoute) {
|
||||
if len(route.HTTPRoute.Route) == 1 {
|
||||
route.HTTPRoute.Route[0].Weight = 100
|
||||
return
|
||||
}
|
||||
|
||||
var weightTotal int32 = 0
|
||||
for _, routeDestination := range route.HTTPRoute.Route {
|
||||
weightTotal += routeDestination.Weight
|
||||
}
|
||||
var sum int32
|
||||
for idx, routeDestination := range route.HTTPRoute.Route {
|
||||
if idx == 0 {
|
||||
continue
|
||||
}
|
||||
weight := float32(routeDestination.Weight) / float32(weightTotal)
|
||||
routeDestination.Weight = int32(weight * 100)
|
||||
sum += routeDestination.Weight
|
||||
}
|
||||
route.HTTPRoute.Route[0].Weight = 100 - sum
|
||||
// Update the recorded status in ingress builder
|
||||
if cache != nil {
|
||||
cache.Update(route)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) applyAppRoot(convertOptions *common.ConvertOptions) {
|
||||
for host, wrapVS := range convertOptions.VirtualServices {
|
||||
if wrapVS.AppRoot != "" {
|
||||
route := &common.WrapperHTTPRoute{
|
||||
HTTPRoute: &networking.HTTPRoute{
|
||||
Name: common.CreateConvertedName(host, "app-root"),
|
||||
Match: []*networking.HTTPMatchRequest{
|
||||
{
|
||||
Uri: &networking.StringMatch{
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: "/",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Redirect: &networking.HTTPRedirect{
|
||||
RedirectCode: 302,
|
||||
Uri: wrapVS.AppRoot,
|
||||
},
|
||||
},
|
||||
WrapperConfig: wrapVS.WrapperConfig,
|
||||
ClusterId: wrapVS.WrapperConfig.AnnotationsConfig.ClusterId,
|
||||
}
|
||||
convertOptions.HTTPRoutes[host] = append([]*common.WrapperHTTPRoute{route}, convertOptions.HTTPRoutes[host]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) applyInternalActiveRedirect(convertOptions *common.ConvertOptions) {
|
||||
for host, routes := range convertOptions.HTTPRoutes {
|
||||
var tempRoutes []*common.WrapperHTTPRoute
|
||||
for _, route := range routes {
|
||||
tempRoutes = append(tempRoutes, route)
|
||||
if route.HTTPRoute.InternalActiveRedirect != nil {
|
||||
fallbackConfig := route.WrapperConfig.AnnotationsConfig.Fallback
|
||||
if fallbackConfig == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
typedNamespace := fallbackConfig.DefaultBackend
|
||||
internalRedirectRoute := route.HTTPRoute.DeepCopy()
|
||||
internalRedirectRoute.Name = internalRedirectRoute.Name + annotations.FallbackRouteNameSuffix
|
||||
internalRedirectRoute.InternalActiveRedirect = nil
|
||||
internalRedirectRoute.Match = []*networking.HTTPMatchRequest{
|
||||
{
|
||||
Uri: &networking.StringMatch{
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: "/",
|
||||
},
|
||||
},
|
||||
Headers: map[string]*networking.StringMatch{
|
||||
annotations.FallbackInjectHeaderRouteName: {
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: internalRedirectRoute.Name,
|
||||
},
|
||||
},
|
||||
annotations.FallbackInjectFallbackService: {
|
||||
MatchType: &networking.StringMatch_Exact{
|
||||
Exact: typedNamespace.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
internalRedirectRoute.Route = []*networking.HTTPRouteDestination{
|
||||
{
|
||||
Destination: &networking.Destination{
|
||||
Host: util.CreateServiceFQDN(typedNamespace.Namespace, typedNamespace.Name),
|
||||
Port: &networking.PortSelector{
|
||||
Number: fallbackConfig.Port,
|
||||
},
|
||||
},
|
||||
Weight: 100,
|
||||
},
|
||||
}
|
||||
|
||||
tempRoutes = append([]*common.WrapperHTTPRoute{{
|
||||
HTTPRoute: internalRedirectRoute,
|
||||
WrapperConfig: route.WrapperConfig,
|
||||
ClusterId: route.ClusterId,
|
||||
}}, tempRoutes...)
|
||||
}
|
||||
}
|
||||
convertOptions.HTTPRoutes[host] = tempRoutes
|
||||
}
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) ReflectSecretChanges(clusterNamespacedName util.ClusterNamespacedName) {
|
||||
var hit bool
|
||||
m.mutex.RLock()
|
||||
if m.watchedSecretSet.Contains(clusterNamespacedName.String()) {
|
||||
hit = true
|
||||
}
|
||||
m.mutex.RUnlock()
|
||||
|
||||
if hit {
|
||||
push := func(kind config.GroupVersionKind) {
|
||||
m.XDSUpdater.ConfigUpdate(&model.PushRequest{
|
||||
Full: true,
|
||||
ConfigsUpdated: map[model.ConfigKey]struct{}{{
|
||||
Kind: kind,
|
||||
Name: clusterNamespacedName.Name,
|
||||
Namespace: clusterNamespacedName.Namespace,
|
||||
}: {}},
|
||||
Reason: []model.TriggerReason{"auth-secret-change"},
|
||||
})
|
||||
}
|
||||
push(gvk.VirtualService)
|
||||
push(gvk.EnvoyFilter)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) Run(stop <-chan struct{}) {}
|
||||
|
||||
func (m *KIngressConfig) HasSynced() bool {
|
||||
IngressLog.Info("In Kingress Synced.")
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
for _, remoteIngressController := range m.remoteIngressControllers {
|
||||
IngressLog.Info("In Kingress Synced.", remoteIngressController)
|
||||
if !remoteIngressController.HasSynced() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
IngressLog.Info("Ingress config controller synced.")
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) SetWatchErrorHandler(f func(r *cache.Reflector, err error)) error {
|
||||
m.WatchErrorHandler = f
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) GetIngressRoutes() model.IngressRouteCollection {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return m.ingressRouteCache
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) GetIngressDomains() model.IngressDomainCollection {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return m.ingressDomainCache
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) Schemas() collection.Schemas {
|
||||
return common.IngressIR
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) Get(config.GroupVersionKind, string, string) *config.Config {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) Create(config.Config) (revision string, err error) {
|
||||
return "", common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) Update(config.Config) (newRevision string, err error) {
|
||||
return "", common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) UpdateStatus(config.Config) (newRevision string, err error) {
|
||||
return "", common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) Patch(config.Config, config.PatchFunc) (string, error) {
|
||||
return "", common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
func (m *KIngressConfig) Delete(config.GroupVersionKind, string, string, *string) error {
|
||||
return common.ErrUnsupportedOp
|
||||
}
|
||||
481
pkg/ingress/config/kingress_config_test.go
Normal file
481
pkg/ingress/config/kingress_config_test.go
Normal file
@@ -0,0 +1,481 @@
|
||||
// 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"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
ingress "knative.dev/networking/pkg/apis/networking/v1alpha1"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/annotations"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/common"
|
||||
kcontrollerv1 "github.com/alibaba/higress/pkg/ingress/kube/kingress"
|
||||
"github.com/alibaba/higress/pkg/kube"
|
||||
)
|
||||
|
||||
func TestNormalizeKWeightedCluster(t *testing.T) {
|
||||
validate := func(route *common.WrapperHTTPRoute) int32 {
|
||||
var total int32
|
||||
fmt.Print("----------------------------")
|
||||
for _, routeDestination := range route.HTTPRoute.Route {
|
||||
total += routeDestination.Weight
|
||||
fmt.Print(routeDestination.Weight)
|
||||
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
var testCases []*common.WrapperHTTPRoute
|
||||
testCases = append(testCases, &common.WrapperHTTPRoute{
|
||||
HTTPRoute: &networking.HTTPRoute{
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{
|
||||
Weight: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
testCases = append(testCases, &common.WrapperHTTPRoute{
|
||||
HTTPRoute: &networking.HTTPRoute{
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{
|
||||
Weight: 98,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
testCases = append(testCases, &common.WrapperHTTPRoute{
|
||||
HTTPRoute: &networking.HTTPRoute{
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{
|
||||
Weight: 0,
|
||||
},
|
||||
{
|
||||
Weight: 48,
|
||||
},
|
||||
{
|
||||
Weight: 48,
|
||||
},
|
||||
},
|
||||
},
|
||||
WeightTotal: 100,
|
||||
})
|
||||
|
||||
testCases = append(testCases, &common.WrapperHTTPRoute{
|
||||
HTTPRoute: &networking.HTTPRoute{
|
||||
Route: []*networking.HTTPRouteDestination{
|
||||
{
|
||||
Weight: 0,
|
||||
},
|
||||
{
|
||||
Weight: 48,
|
||||
},
|
||||
{
|
||||
Weight: 48,
|
||||
},
|
||||
},
|
||||
},
|
||||
WeightTotal: 80,
|
||||
})
|
||||
|
||||
for _, route := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
normalizeWeightedKCluster(nil, route)
|
||||
if validate(route) != 100 {
|
||||
t.Fatalf("Weight sum should be 100, but actual is %d", validate(route))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertGatewaysForKIngress(t *testing.T) {
|
||||
fake := kube.NewFakeClient()
|
||||
v1Options := common.Options{
|
||||
Enable: true,
|
||||
ClusterId: "kingress",
|
||||
RawClusterId: "kingress__",
|
||||
}
|
||||
kingressV1Controller := kcontrollerv1.NewController(fake, fake, v1Options, nil)
|
||||
m := NewKIngressConfig(fake, nil, "wakanda", "gw-123-istio")
|
||||
m.remoteIngressControllers = map[string]common.KIngressController{
|
||||
"kingress": kingressV1Controller,
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
inputConfig []common.WrapperConfig
|
||||
expect map[string]config.Config
|
||||
}{
|
||||
{
|
||||
name: "kingress",
|
||||
inputConfig: []common.WrapperConfig{
|
||||
{
|
||||
Config: &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "test-1",
|
||||
Namespace: "wakanda",
|
||||
Annotations: map[string]string{
|
||||
common.ClusterIdAnnotation: "kingress",
|
||||
},
|
||||
},
|
||||
Spec: ingress.IngressSpec{
|
||||
HTTPOption: ingress.HTTPOptionEnabled,
|
||||
TLS: []ingress.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"test.com"},
|
||||
SecretName: "test-com",
|
||||
},
|
||||
},
|
||||
Rules: []ingress.IngressRule{
|
||||
{
|
||||
Hosts: []string{"foo.com"},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
Splits: []ingress.IngressBackendSplit{{
|
||||
IngressBackend: ingress.IngressBackend{
|
||||
ServiceNamespace: "wakanda",
|
||||
ServiceName: "test-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityExternalIP,
|
||||
},
|
||||
{
|
||||
Hosts: []string{"test.com"},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
Splits: []ingress.IngressBackendSplit{{
|
||||
IngressBackend: ingress.IngressBackend{
|
||||
ServiceNamespace: "wakanda",
|
||||
ServiceName: "test-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityExternalIP,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AnnotationsConfig: &annotations.Ingress{},
|
||||
},
|
||||
{
|
||||
Config: &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "test-2",
|
||||
Namespace: "wakanda",
|
||||
Annotations: map[string]string{
|
||||
common.ClusterIdAnnotation: "kingress",
|
||||
},
|
||||
},
|
||||
Spec: ingress.IngressSpec{
|
||||
HTTPOption: ingress.HTTPOptionRedirected,
|
||||
TLS: []ingress.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"foo.com"},
|
||||
SecretName: "foo-com",
|
||||
},
|
||||
{
|
||||
Hosts: []string{"test.com"},
|
||||
SecretName: "test-com-2",
|
||||
},
|
||||
},
|
||||
Rules: []ingress.IngressRule{
|
||||
{
|
||||
Hosts: []string{"foo.com"},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
Splits: []ingress.IngressBackendSplit{{
|
||||
IngressBackend: ingress.IngressBackend{
|
||||
ServiceNamespace: "wakanda",
|
||||
ServiceName: "test-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityExternalIP,
|
||||
},
|
||||
{
|
||||
Hosts: []string{"bar.com"},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
Splits: []ingress.IngressBackendSplit{{
|
||||
IngressBackend: ingress.IngressBackend{
|
||||
ServiceNamespace: "wakanda",
|
||||
ServiceName: "test-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityExternalIP,
|
||||
},
|
||||
{
|
||||
Hosts: []string{"test.com"},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
Splits: []ingress.IngressBackendSplit{{
|
||||
IngressBackend: ingress.IngressBackend{
|
||||
ServiceNamespace: "wakanda",
|
||||
ServiceName: "test-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityExternalIP,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AnnotationsConfig: &annotations.Ingress{},
|
||||
},
|
||||
{
|
||||
Config: &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "test-3",
|
||||
Namespace: "wakanda",
|
||||
Annotations: map[string]string{
|
||||
common.ClusterIdAnnotation: "kingress",
|
||||
},
|
||||
},
|
||||
Spec: ingress.IngressSpec{
|
||||
HTTPOption: ingress.HTTPOptionEnabled,
|
||||
TLS: []ingress.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"foo.com"},
|
||||
SecretName: "foo-com",
|
||||
},
|
||||
{
|
||||
Hosts: []string{"test.com"},
|
||||
SecretName: "test-com-3",
|
||||
},
|
||||
},
|
||||
Rules: []ingress.IngressRule{
|
||||
{
|
||||
Hosts: []string{"foo.com"},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
Splits: []ingress.IngressBackendSplit{{
|
||||
IngressBackend: ingress.IngressBackend{
|
||||
ServiceNamespace: "wakanda",
|
||||
ServiceName: "test-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityExternalIP,
|
||||
},
|
||||
{
|
||||
Hosts: []string{"bar.com"},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
Splits: []ingress.IngressBackendSplit{{
|
||||
IngressBackend: ingress.IngressBackend{
|
||||
ServiceNamespace: "wakanda",
|
||||
ServiceName: "test-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityExternalIP,
|
||||
},
|
||||
{
|
||||
Hosts: []string{"test.com"},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{
|
||||
{
|
||||
Path: "/test",
|
||||
Splits: []ingress.IngressBackendSplit{{
|
||||
IngressBackend: ingress.IngressBackend{
|
||||
ServiceNamespace: "wakanda",
|
||||
ServiceName: "test-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityExternalIP,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AnnotationsConfig: &annotations.Ingress{},
|
||||
},
|
||||
},
|
||||
expect: map[string]config.Config{
|
||||
"foo.com": {
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.Gateway,
|
||||
Name: "istio-autogenerated-k8s-ingress-foo-com",
|
||||
Namespace: "wakanda",
|
||||
Annotations: map[string]string{
|
||||
common.ClusterIdAnnotation: "kingress",
|
||||
common.HostAnnotation: "foo.com",
|
||||
},
|
||||
},
|
||||
Spec: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Number: 80,
|
||||
Protocol: "HTTP",
|
||||
Name: "http-80-ingress-kingress-wakanda-test-1-foo-com",
|
||||
},
|
||||
Hosts: []string{"foo.com"},
|
||||
//Tls: &networking.ServerTLSSettings{
|
||||
// HttpsRedirect: true,
|
||||
//},
|
||||
},
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Number: 443,
|
||||
Protocol: "HTTPS",
|
||||
Name: "https-443-ingress-kingress-wakanda-test-2-foo-com",
|
||||
},
|
||||
Hosts: []string{"foo.com"},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
CredentialName: "kubernetes-ingress://kingress__/wakanda/foo-com",
|
||||
//CipherSuites: []string{"ECDHE-RSA-AES128-GCM-SHA256", "AES256-SHA"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"test.com": {
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.Gateway,
|
||||
Name: "istio-autogenerated-k8s-ingress-test-com",
|
||||
Namespace: "wakanda",
|
||||
Annotations: map[string]string{
|
||||
common.ClusterIdAnnotation: "kingress",
|
||||
common.HostAnnotation: "test.com",
|
||||
},
|
||||
},
|
||||
Spec: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Number: 80,
|
||||
Protocol: "HTTP",
|
||||
Name: "http-80-ingress-kingress-wakanda-test-1-test-com",
|
||||
},
|
||||
Hosts: []string{"test.com"},
|
||||
//Tls: &networking.ServerTLSSettings{
|
||||
// HttpsRedirect: true,
|
||||
//},
|
||||
},
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Number: 443,
|
||||
Protocol: "HTTPS",
|
||||
Name: "https-443-ingress-kingress-wakanda-test-1-test-com",
|
||||
},
|
||||
Hosts: []string{"test.com"},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
CredentialName: "kubernetes-ingress://kingress__/wakanda/test-com",
|
||||
//CipherSuites: []string{"ECDHE-RSA-AES128-GCM-SHA256", "AES256-SHA"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"bar.com": {
|
||||
Meta: config.Meta{
|
||||
GroupVersionKind: gvk.Gateway,
|
||||
Name: "istio-autogenerated-k8s-ingress-bar-com",
|
||||
Namespace: "wakanda",
|
||||
Annotations: map[string]string{
|
||||
common.ClusterIdAnnotation: "kingress",
|
||||
common.HostAnnotation: "bar.com",
|
||||
},
|
||||
},
|
||||
Spec: &networking.Gateway{
|
||||
Servers: []*networking.Server{
|
||||
{
|
||||
Port: &networking.Port{
|
||||
Number: 80,
|
||||
Protocol: "HTTP",
|
||||
Name: "http-80-ingress-kingress-wakanda-test-2-bar-com",
|
||||
},
|
||||
Hosts: []string{"bar.com"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
result := m.convertGateways(testCase.inputConfig)
|
||||
|
||||
target := map[string]config.Config{}
|
||||
for _, item := range result {
|
||||
host := common.GetHost(item.Annotations)
|
||||
fmt.Print(item)
|
||||
target[host] = item
|
||||
}
|
||||
assert.Equal(t, testCase.expect, target)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -139,3 +139,27 @@ type IngressController interface {
|
||||
// HasSynced returns true after initial cache synchronization is complete
|
||||
HasSynced() bool
|
||||
}
|
||||
|
||||
type KIngressController interface {
|
||||
// RegisterEventHandler adds a handler to receive config update events for a
|
||||
// configuration type
|
||||
RegisterEventHandler(kind config.GroupVersionKind, handler model.EventHandler)
|
||||
|
||||
List() []config.Config
|
||||
|
||||
ServiceLister() listerv1.ServiceLister
|
||||
|
||||
SecretLister() listerv1.SecretLister
|
||||
|
||||
ConvertGateway(convertOptions *ConvertOptions, wrapper *WrapperConfig) error
|
||||
|
||||
ConvertHTTPRoute(convertOptions *ConvertOptions, wrapper *WrapperConfig) error
|
||||
|
||||
// Run until a signal is received
|
||||
Run(stop <-chan struct{})
|
||||
|
||||
SetWatchErrorHandler(func(r *cache.Reflector, err error)) error
|
||||
|
||||
// HasSynced returns true after initial cache synchronization is complete
|
||||
HasSynced() bool
|
||||
}
|
||||
|
||||
751
pkg/ingress/kube/kingress/controller.go
Normal file
751
pkg/ingress/kube/kingress/controller.go
Normal file
@@ -0,0 +1,751 @@
|
||||
// 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 kingress
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/annotations"
|
||||
"path"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/alibaba/higress/pkg/kube"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
networking "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pilot/pkg/model/credentials"
|
||||
"istio.io/istio/pilot/pkg/util/sets"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/constants"
|
||||
"istio.io/istio/pkg/config/protocol"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"istio.io/istio/pkg/kube/controllers"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
kset "k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
listerv1 "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
ingress "knative.dev/networking/pkg/apis/networking/v1alpha1"
|
||||
networkingv1alpha1 "knative.dev/networking/pkg/client/listers/networking/v1alpha1"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/common"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/kingress/resources"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/secret"
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
)
|
||||
|
||||
var (
|
||||
_ common.KIngressController = &controller{}
|
||||
)
|
||||
|
||||
const (
|
||||
// ClassAnnotationKey points to the annotation for the class of this resource.
|
||||
ClassAnnotationKey = "networking.knative.dev/ingress.class"
|
||||
IngressClassName = "higress"
|
||||
)
|
||||
|
||||
type controller struct {
|
||||
queue workqueue.RateLimitingInterface
|
||||
virtualServiceHandlers []model.EventHandler
|
||||
gatewayHandlers []model.EventHandler
|
||||
envoyFilterHandlers []model.EventHandler
|
||||
|
||||
options common.Options
|
||||
|
||||
mutex sync.RWMutex
|
||||
// key: namespace/name
|
||||
ingresses map[string]*ingress.Ingress
|
||||
|
||||
ingressInformer cache.SharedInformer
|
||||
ingressLister networkingv1alpha1.IngressLister
|
||||
serviceInformer cache.SharedInformer
|
||||
serviceLister listerv1.ServiceLister
|
||||
secretController secret.SecretController
|
||||
statusSyncer *statusSyncer
|
||||
}
|
||||
|
||||
// NewController creates a new Kubernetes controller
|
||||
func NewController(localKubeClient, client kube.Client, options common.Options,
|
||||
secretController secret.SecretController) common.KIngressController {
|
||||
q := workqueue.NewRateLimitingQueue(workqueue.DefaultItemBasedRateLimiter())
|
||||
|
||||
//var namespace string = "default"
|
||||
ingressInformer := client.KIngressInformer().Networking().V1alpha1().Ingresses()
|
||||
serviceInformer := client.KubeInformer().Core().V1().Services()
|
||||
|
||||
c := &controller{
|
||||
options: options,
|
||||
queue: q,
|
||||
ingresses: make(map[string]*ingress.Ingress),
|
||||
ingressInformer: ingressInformer.Informer(),
|
||||
ingressLister: ingressInformer.Lister(),
|
||||
serviceInformer: serviceInformer.Informer(),
|
||||
serviceLister: serviceInformer.Lister(),
|
||||
secretController: secretController,
|
||||
}
|
||||
|
||||
handler := controllers.LatestVersionHandlerFuncs(controllers.EnqueueForSelf(q))
|
||||
c.ingressInformer.AddEventHandler(handler)
|
||||
|
||||
if options.EnableStatus {
|
||||
c.statusSyncer = newStatusSyncer(localKubeClient, client, c, options.SystemNamespace)
|
||||
} else {
|
||||
IngressLog.Infof("Disable status update for cluster %s", options.ClusterId)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *controller) ServiceLister() listerv1.ServiceLister {
|
||||
return c.serviceLister
|
||||
}
|
||||
|
||||
func (c *controller) SecretLister() listerv1.SecretLister {
|
||||
return c.secretController.Lister()
|
||||
}
|
||||
|
||||
func (c *controller) Run(stop <-chan struct{}) {
|
||||
if c.statusSyncer != nil {
|
||||
go c.statusSyncer.run(stop)
|
||||
}
|
||||
go c.secretController.Run(stop)
|
||||
|
||||
defer utilruntime.HandleCrash()
|
||||
defer c.queue.ShutDown()
|
||||
|
||||
if !cache.WaitForCacheSync(stop, c.HasSynced) {
|
||||
IngressLog.Errorf("Failed to sync ingress controller cache for cluster %s", c.options.ClusterId)
|
||||
return
|
||||
}
|
||||
go wait.Until(c.worker, time.Second, stop)
|
||||
<-stop
|
||||
}
|
||||
|
||||
func (c *controller) worker() {
|
||||
for c.processNextWorkItem() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *controller) processNextWorkItem() bool {
|
||||
key, quit := c.queue.Get()
|
||||
if quit {
|
||||
return false
|
||||
}
|
||||
defer c.queue.Done(key)
|
||||
ingressNamespacedName := key.(types.NamespacedName)
|
||||
if err := c.onEvent(ingressNamespacedName); err != nil {
|
||||
IngressLog.Errorf("error processing ingress item (%v) (retrying): %v, cluster: %s", key, err, c.options.ClusterId)
|
||||
c.queue.AddRateLimited(key)
|
||||
} else {
|
||||
c.queue.Forget(key)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *controller) onEvent(namespacedName types.NamespacedName) error {
|
||||
event := model.EventUpdate
|
||||
ing, err := c.ingressLister.Ingresses(namespacedName.Namespace).Get(namespacedName.Name)
|
||||
ing.Status.InitializeConditions()
|
||||
if err != nil {
|
||||
if kerrors.IsNotFound(err) {
|
||||
event = model.EventDelete
|
||||
c.mutex.Lock()
|
||||
ing = c.ingresses[namespacedName.String()]
|
||||
delete(c.ingresses, namespacedName.String())
|
||||
c.mutex.Unlock()
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// ingress deleted, and it is not processed before
|
||||
if ing == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// we should check need process only when event is not delete,
|
||||
// if it is delete event, and previously processed, we need to process too.
|
||||
if event != model.EventDelete {
|
||||
shouldProcess, err := c.shouldProcessIngressUpdate(ing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !shouldProcess {
|
||||
IngressLog.Infof("no need process, ingress %s", namespacedName)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
vsmetadata := config.Meta{
|
||||
Name: ing.Name + "-" + "virtualservice",
|
||||
Namespace: ing.Namespace,
|
||||
GroupVersionKind: gvk.VirtualService,
|
||||
// Set this label so that we do not compare configs and just push.
|
||||
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
|
||||
}
|
||||
efmetadata := config.Meta{
|
||||
Name: ing.Name + "-" + "envoyfilter",
|
||||
Namespace: ing.Namespace,
|
||||
GroupVersionKind: gvk.EnvoyFilter,
|
||||
// Set this label so that we do not compare configs and just push.
|
||||
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
|
||||
}
|
||||
gatewaymetadata := config.Meta{
|
||||
Name: ing.Name + "-" + "gateway",
|
||||
Namespace: ing.Namespace,
|
||||
GroupVersionKind: gvk.Gateway,
|
||||
// Set this label so that we do not compare configs and just push.
|
||||
Labels: map[string]string{constants.AlwaysPushLabel: "true"},
|
||||
}
|
||||
|
||||
for _, f := range c.virtualServiceHandlers {
|
||||
f(config.Config{Meta: vsmetadata}, config.Config{Meta: vsmetadata}, event)
|
||||
}
|
||||
|
||||
for _, f := range c.envoyFilterHandlers {
|
||||
f(config.Config{Meta: efmetadata}, config.Config{Meta: efmetadata}, event)
|
||||
}
|
||||
|
||||
for _, f := range c.gatewayHandlers {
|
||||
f(config.Config{Meta: gatewaymetadata}, config.Config{Meta: gatewaymetadata}, event)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *controller) RegisterEventHandler(kind config.GroupVersionKind, f model.EventHandler) {
|
||||
switch kind {
|
||||
case gvk.VirtualService:
|
||||
c.virtualServiceHandlers = append(c.virtualServiceHandlers, f)
|
||||
case gvk.Gateway:
|
||||
c.gatewayHandlers = append(c.gatewayHandlers, f)
|
||||
case gvk.EnvoyFilter:
|
||||
c.envoyFilterHandlers = append(c.envoyFilterHandlers, f)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *controller) SetWatchErrorHandler(handler func(r *cache.Reflector, err error)) error {
|
||||
var errs error
|
||||
if err := c.serviceInformer.SetWatchErrorHandler(handler); err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
}
|
||||
if err := c.ingressInformer.SetWatchErrorHandler(handler); err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
}
|
||||
if err := c.secretController.Informer().SetWatchErrorHandler(handler); err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func (c *controller) HasSynced() bool {
|
||||
return c.ingressInformer.HasSynced() && c.serviceInformer.HasSynced() && c.secretController.HasSynced()
|
||||
}
|
||||
|
||||
func (c *controller) List() []config.Config {
|
||||
c.mutex.RLock()
|
||||
out := make([]config.Config, 0, len(c.ingresses))
|
||||
c.mutex.RUnlock()
|
||||
|
||||
for _, raw := range c.ingressInformer.GetStore().List() {
|
||||
ing, ok := raw.(*ingress.Ingress)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if should, err := c.shouldProcessIngress(ing); !should || err != nil {
|
||||
continue
|
||||
}
|
||||
copiedConfig := ing.DeepCopy()
|
||||
|
||||
outConfig := config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: copiedConfig.Name,
|
||||
Namespace: copiedConfig.Namespace,
|
||||
Annotations: common.CreateOrUpdateAnnotations(copiedConfig.Annotations, c.options),
|
||||
Labels: copiedConfig.Labels,
|
||||
CreationTimestamp: copiedConfig.CreationTimestamp.Time,
|
||||
},
|
||||
Spec: copiedConfig.Spec,
|
||||
}
|
||||
|
||||
out = append(out, outConfig)
|
||||
}
|
||||
|
||||
common.RecordIngressNumber(c.options.ClusterId, len(out))
|
||||
return out
|
||||
}
|
||||
|
||||
func extractTLSSecretName(host string, tls []ingress.IngressTLS) string {
|
||||
if len(tls) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, t := range tls {
|
||||
match := false
|
||||
for _, h := range t.Hosts {
|
||||
if h == host {
|
||||
match = true
|
||||
}
|
||||
}
|
||||
|
||||
if match {
|
||||
return t.SecretName
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *controller) ConvertGateway(convertOptions *common.ConvertOptions, wrapper *common.WrapperConfig) error {
|
||||
if convertOptions == nil {
|
||||
return fmt.Errorf("convertOptions is nil")
|
||||
}
|
||||
if wrapper == nil {
|
||||
return fmt.Errorf("wrapperConfig is nil")
|
||||
}
|
||||
|
||||
cfg := wrapper.Config
|
||||
kingressv1alpha1, ok := cfg.Spec.(ingress.IngressSpec)
|
||||
|
||||
if !ok {
|
||||
common.IncrementInvalidIngress(c.options.ClusterId, common.Unknown)
|
||||
return fmt.Errorf("convert type is invalid in cluster %s", c.options.ClusterId)
|
||||
}
|
||||
if len(kingressv1alpha1.Rules) == 0 {
|
||||
common.IncrementInvalidIngress(c.options.ClusterId, common.EmptyRule)
|
||||
return fmt.Errorf("invalid ingress rule %s:%s in cluster %s, `rules` must be specified", cfg.Namespace, cfg.Name, c.options.ClusterId)
|
||||
}
|
||||
|
||||
for _, rule := range kingressv1alpha1.Rules {
|
||||
for _, ruleHost := range rule.Hosts {
|
||||
cleanHost := common.CleanHost(ruleHost)
|
||||
// Need create builder for every rule.
|
||||
domainBuilder := &common.IngressDomainBuilder{
|
||||
ClusterId: c.options.ClusterId,
|
||||
Protocol: common.HTTP,
|
||||
Host: ruleHost,
|
||||
Ingress: cfg,
|
||||
Event: common.Normal,
|
||||
}
|
||||
// Extract the previous gateway and builder
|
||||
wrapperGateway, exist := convertOptions.Gateways[ruleHost]
|
||||
preDomainBuilder, _ := convertOptions.IngressDomainCache.Valid[ruleHost]
|
||||
if !exist {
|
||||
wrapperGateway = &common.WrapperGateway{
|
||||
Gateway: &networking.Gateway{},
|
||||
WrapperConfig: wrapper,
|
||||
ClusterId: c.options.ClusterId,
|
||||
Host: ruleHost,
|
||||
}
|
||||
if c.options.GatewaySelectorKey != "" {
|
||||
wrapperGateway.Gateway.Selector = map[string]string{c.options.GatewaySelectorKey: c.options.GatewaySelectorValue}
|
||||
}
|
||||
if rule.Visibility == ingress.IngressVisibilityClusterLocal {
|
||||
wrapperGateway.Gateway.Servers = append(wrapperGateway.Gateway.Servers, &networking.Server{
|
||||
Port: &networking.Port{
|
||||
Number: 8081,
|
||||
Protocol: string(protocol.HTTP),
|
||||
Name: common.CreateConvertedName("http-8081-ingress", c.options.ClusterId, cfg.Namespace, cfg.Name, cleanHost),
|
||||
},
|
||||
Hosts: []string{ruleHost},
|
||||
})
|
||||
|
||||
} else {
|
||||
wrapperGateway.Gateway.Servers = append(wrapperGateway.Gateway.Servers, &networking.Server{
|
||||
Port: &networking.Port{
|
||||
Number: 80,
|
||||
Protocol: string(protocol.HTTP),
|
||||
Name: common.CreateConvertedName("http-80-ingress", c.options.ClusterId, cfg.Namespace, cfg.Name, cleanHost),
|
||||
},
|
||||
Hosts: []string{ruleHost},
|
||||
})
|
||||
}
|
||||
|
||||
// Add new gateway, builder
|
||||
convertOptions.Gateways[ruleHost] = wrapperGateway
|
||||
convertOptions.IngressDomainCache.Valid[ruleHost] = domainBuilder
|
||||
} else {
|
||||
// Fallback to get downstream tls from current ingress.
|
||||
if wrapperGateway.WrapperConfig.AnnotationsConfig.DownstreamTLS == nil {
|
||||
wrapperGateway.WrapperConfig.AnnotationsConfig.DownstreamTLS = wrapper.AnnotationsConfig.DownstreamTLS
|
||||
}
|
||||
}
|
||||
//Redirect option
|
||||
if isIngressPublic(&kingressv1alpha1) && (kingressv1alpha1.HTTPOption == ingress.HTTPOptionRedirected) {
|
||||
for _, server := range wrapperGateway.Gateway.Servers {
|
||||
if protocol.Parse(server.Port.Protocol).IsHTTP() {
|
||||
server.Tls = &networking.ServerTLSSettings{
|
||||
HttpsRedirect: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if isIngressPublic(&kingressv1alpha1) && (kingressv1alpha1.HTTPOption == ingress.HTTPOptionEnabled) {
|
||||
for _, server := range wrapperGateway.Gateway.Servers {
|
||||
if protocol.Parse(server.Port.Protocol).IsHTTP() {
|
||||
server.Tls = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// There are no tls settings, so just skip.
|
||||
if len(kingressv1alpha1.TLS) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get tls secret matching the rule host
|
||||
secretName := extractTLSSecretName(ruleHost, kingressv1alpha1.TLS)
|
||||
if secretName == "" {
|
||||
// There no matching secret, so just skip.
|
||||
continue
|
||||
}
|
||||
|
||||
domainBuilder.Protocol = common.HTTPS
|
||||
domainBuilder.SecretName = path.Join(c.options.ClusterId, cfg.Namespace, secretName)
|
||||
|
||||
// There is a matching secret and the gateway has already a tls secret.
|
||||
// We should report the duplicated tls secret event.
|
||||
if wrapperGateway.IsHTTPS() {
|
||||
domainBuilder.Event = common.DuplicatedTls
|
||||
domainBuilder.PreIngress = preDomainBuilder.Ingress
|
||||
convertOptions.IngressDomainCache.Invalid = append(convertOptions.IngressDomainCache.Invalid,
|
||||
domainBuilder.Build())
|
||||
continue
|
||||
}
|
||||
|
||||
// Append https server
|
||||
wrapperGateway.Gateway.Servers = append(wrapperGateway.Gateway.Servers, &networking.Server{
|
||||
Port: &networking.Port{
|
||||
Number: 443,
|
||||
Protocol: string(protocol.HTTPS),
|
||||
Name: common.CreateConvertedName("https-443-ingress", c.options.ClusterId, cfg.Namespace, cfg.Name, cleanHost),
|
||||
},
|
||||
Hosts: []string{ruleHost},
|
||||
Tls: &networking.ServerTLSSettings{
|
||||
Mode: networking.ServerTLSSettings_SIMPLE,
|
||||
CredentialName: credentials.ToKubernetesIngressResource(c.options.RawClusterId, cfg.Namespace, secretName),
|
||||
},
|
||||
})
|
||||
|
||||
// Update domain builder
|
||||
convertOptions.IngressDomainCache.Valid[ruleHost] = domainBuilder
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *controller) ConvertHTTPRoute(convertOptions *common.ConvertOptions, wrapper *common.WrapperConfig) error {
|
||||
if convertOptions == nil {
|
||||
return fmt.Errorf("convertOptions is nil")
|
||||
}
|
||||
if wrapper == nil {
|
||||
return fmt.Errorf("wrapperConfig is nil")
|
||||
}
|
||||
|
||||
cfg := wrapper.Config
|
||||
KingressV1, ok := cfg.Spec.(ingress.IngressSpec)
|
||||
if !ok {
|
||||
common.IncrementInvalidIngress(c.options.ClusterId, common.Unknown)
|
||||
return fmt.Errorf("convert type is invalid in cluster %s", c.options.ClusterId)
|
||||
}
|
||||
if len(KingressV1.Rules) == 0 {
|
||||
common.IncrementInvalidIngress(c.options.ClusterId, common.EmptyRule)
|
||||
return fmt.Errorf("invalid ingress rule %s:%s in cluster %s, `rules` must be specified", cfg.Namespace, cfg.Name, c.options.ClusterId)
|
||||
}
|
||||
convertOptions.HasDefaultBackend = false
|
||||
// In one ingress, we will limit the rule conflict.
|
||||
// When the host, pathType, path of two rule are same, we think there is a conflict event.
|
||||
definedRules := sets.NewSet()
|
||||
|
||||
var (
|
||||
// But in across ingresses case, we will restrict this limit.
|
||||
// When the {host, path, headers, method, params} of two rule in different ingress are same, we think there is a conflict event.
|
||||
tempRuleKey []string
|
||||
)
|
||||
|
||||
for _, rule := range KingressV1.Rules {
|
||||
for _, rulehost := range rule.Hosts {
|
||||
if rule.HTTP == nil || len(rule.HTTP.Paths) == 0 {
|
||||
IngressLog.Warnf("invalid ingress rule %s:%s for host %q in cluster %s, no paths defined", cfg.Namespace, cfg.Name, rulehost, c.options.ClusterId)
|
||||
continue
|
||||
}
|
||||
wrapperVS, exist := convertOptions.VirtualServices[rulehost]
|
||||
if !exist {
|
||||
wrapperVS = &common.WrapperVirtualService{
|
||||
VirtualService: &networking.VirtualService{
|
||||
Hosts: []string{rulehost},
|
||||
},
|
||||
WrapperConfig: wrapper,
|
||||
}
|
||||
convertOptions.VirtualServices[rulehost] = wrapperVS
|
||||
}
|
||||
wrapperHttpRoutes := make([]*common.WrapperHTTPRoute, 0, len(rule.HTTP.Paths))
|
||||
for _, httpPath := range rule.HTTP.Paths {
|
||||
wrapperHttpRoute := &common.WrapperHTTPRoute{
|
||||
HTTPRoute: &networking.HTTPRoute{},
|
||||
WrapperConfig: wrapper,
|
||||
Host: rulehost,
|
||||
ClusterId: c.options.ClusterId,
|
||||
}
|
||||
|
||||
var pathType common.PathType
|
||||
originPath := httpPath.Path
|
||||
pathType = common.Prefix
|
||||
wrapperHttpRoute.OriginPath = originPath
|
||||
wrapperHttpRoute.OriginPathType = pathType
|
||||
wrapperHttpRoute.HTTPRoute = resources.MakeVirtualServiceRoute(transformHosts(rulehost), &httpPath)
|
||||
wrapperHttpRoute.HTTPRoute.Name = common.GenerateUniqueRouteName(c.options.SystemNamespace, wrapperHttpRoute)
|
||||
ingressRouteBuilder := convertOptions.IngressRouteCache.New(wrapperHttpRoute)
|
||||
hostAndPath := wrapperHttpRoute.PathFormat()
|
||||
key := createRuleKey(cfg.Annotations, hostAndPath)
|
||||
wrapperHttpRoute.RuleKey = key
|
||||
if WrapPreIngress, exist := convertOptions.Route2Ingress[key]; exist {
|
||||
ingressRouteBuilder.PreIngress = WrapPreIngress.Config
|
||||
ingressRouteBuilder.Event = common.DuplicatedRoute
|
||||
}
|
||||
tempRuleKey = append(tempRuleKey, key)
|
||||
|
||||
// Two duplicated rules in the same ingress.
|
||||
if ingressRouteBuilder.Event == common.Normal {
|
||||
pathFormat := wrapperHttpRoute.PathFormat()
|
||||
if definedRules.Contains(pathFormat) {
|
||||
ingressRouteBuilder.PreIngress = cfg
|
||||
ingressRouteBuilder.Event = common.DuplicatedRoute
|
||||
}
|
||||
definedRules.Insert(pathFormat)
|
||||
}
|
||||
|
||||
// backend service check
|
||||
var event common.Event
|
||||
destinationConfig := wrapper.AnnotationsConfig.Destination
|
||||
event = c.IngressRouteBuilderServicesCheck(&httpPath, cfg.Namespace, ingressRouteBuilder, destinationConfig)
|
||||
|
||||
if destinationConfig != nil {
|
||||
wrapperHttpRoute.WeightTotal = int32(destinationConfig.WeightSum)
|
||||
}
|
||||
|
||||
if ingressRouteBuilder.Event != common.Normal {
|
||||
event = ingressRouteBuilder.Event
|
||||
}
|
||||
|
||||
if event != common.Normal {
|
||||
common.IncrementInvalidIngress(c.options.ClusterId, event)
|
||||
ingressRouteBuilder.Event = event
|
||||
} else {
|
||||
wrapperHttpRoutes = append(wrapperHttpRoutes, wrapperHttpRoute)
|
||||
}
|
||||
convertOptions.IngressRouteCache.Add(ingressRouteBuilder)
|
||||
}
|
||||
|
||||
for idx, item := range tempRuleKey {
|
||||
if val, exist := convertOptions.Route2Ingress[item]; !exist || strings.Compare(val.RuleKey, tempRuleKey[idx]) != 0 {
|
||||
convertOptions.Route2Ingress[item] = &common.WrapperConfigWithRuleKey{
|
||||
Config: cfg,
|
||||
RuleKey: tempRuleKey[idx],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
old, f := convertOptions.HTTPRoutes[rulehost]
|
||||
if f {
|
||||
old = append(old, wrapperHttpRoutes...)
|
||||
convertOptions.HTTPRoutes[rulehost] = old
|
||||
} else {
|
||||
convertOptions.HTTPRoutes[rulehost] = wrapperHttpRoutes
|
||||
}
|
||||
|
||||
// Sort, exact -> prefix -> regex
|
||||
routes := convertOptions.HTTPRoutes[rulehost]
|
||||
IngressLog.Debugf("routes of host %s is %v", rulehost, routes)
|
||||
common.SortHTTPRoutes(routes)
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (c *controller) IngressRouteBuilderServicesCheck(httppath *ingress.HTTPIngressPath, namespace string,
|
||||
builder *common.IngressRouteBuilder, config *annotations.DestinationConfig) common.Event {
|
||||
|
||||
//backend check
|
||||
if httppath.Splits == nil {
|
||||
return common.InvalidBackendService
|
||||
}
|
||||
for _, split := range httppath.Splits {
|
||||
if split.ServiceName == "" {
|
||||
return common.InvalidBackendService
|
||||
}
|
||||
backendService := model.BackendService{
|
||||
Namespace: namespace,
|
||||
Name: split.ServiceName,
|
||||
Port: uint32(split.ServicePort.IntValue()),
|
||||
Weight: int32(split.Percent),
|
||||
}
|
||||
builder.ServiceList = append(builder.ServiceList, backendService)
|
||||
}
|
||||
return common.Normal
|
||||
}
|
||||
|
||||
func (c *controller) shouldProcessIngressWithClass(ing *ingress.Ingress) bool {
|
||||
if classValue, found := ing.GetAnnotations()[ClassAnnotationKey]; !found || classValue != IngressClassName {
|
||||
IngressLog.Debugf("Ingress class %s does not match knative IngressCLassName %s.", classValue, IngressClassName)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *controller) shouldProcessIngress(i *ingress.Ingress) (bool, error) {
|
||||
//check namespace
|
||||
if c.shouldProcessIngressWithClass(i) {
|
||||
switch c.options.WatchNamespace {
|
||||
case "":
|
||||
return true, nil
|
||||
default:
|
||||
return c.options.WatchNamespace == i.Namespace, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
|
||||
}
|
||||
|
||||
// shouldProcessIngressUpdate checks whether we should renotify registered handlers about an update event
|
||||
func (c *controller) shouldProcessIngressUpdate(ing *ingress.Ingress) (bool, error) {
|
||||
shouldProcess, err := c.shouldProcessIngress(ing)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
namespacedName := ing.Namespace + "/" + ing.Name
|
||||
if shouldProcess {
|
||||
// record processed ingress
|
||||
c.mutex.Lock()
|
||||
preConfig, exist := c.ingresses[namespacedName]
|
||||
c.ingresses[namespacedName] = ing
|
||||
c.mutex.Unlock()
|
||||
|
||||
// We only care about annotations, labels and spec.
|
||||
if exist {
|
||||
if !reflect.DeepEqual(preConfig.Annotations, ing.Annotations) {
|
||||
IngressLog.Debugf("Annotations of ingress %s changed, should process.", namespacedName)
|
||||
return true, nil
|
||||
}
|
||||
if !reflect.DeepEqual(preConfig.Labels, ing.Labels) {
|
||||
IngressLog.Debugf("Labels of ingress %s changed, should process.", namespacedName)
|
||||
return true, nil
|
||||
}
|
||||
if !reflect.DeepEqual(preConfig.Spec, ing.Spec) {
|
||||
IngressLog.Debugf("Spec of ingress %s changed, should process.", namespacedName)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
IngressLog.Debugf("First receive relative ingress %s, should process.", namespacedName)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
_, preProcessed := c.ingresses[namespacedName]
|
||||
// previous processed but should not currently, delete it
|
||||
if preProcessed && !shouldProcess {
|
||||
delete(c.ingresses, namespacedName)
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
|
||||
return preProcessed, nil
|
||||
}
|
||||
|
||||
// createRuleKey according to the pathType, path, methods, headers, params of rules
|
||||
func createRuleKey(annots map[string]string, hostAndPath string) string {
|
||||
var (
|
||||
headers [][2]string
|
||||
params [][2]string
|
||||
sb strings.Builder
|
||||
)
|
||||
|
||||
sep := "\n\n"
|
||||
|
||||
// path
|
||||
sb.WriteString(hostAndPath)
|
||||
sb.WriteString(sep)
|
||||
|
||||
// methods
|
||||
if str, ok := annots[annotations.HigressAnnotationsPrefix+"/"+annotations.MatchMethod]; ok {
|
||||
sb.WriteString(str)
|
||||
}
|
||||
sb.WriteString(sep)
|
||||
|
||||
start := len(annotations.HigressAnnotationsPrefix) + 1 // example: higress.io/exact-match-header-key: value
|
||||
// headers && params
|
||||
for k, val := range annots {
|
||||
if idx := strings.Index(k, annotations.MatchHeader); idx != -1 {
|
||||
key := k[start:idx] + k[idx+len(annotations.MatchHeader)+1:]
|
||||
headers = append(headers, [2]string{key, val})
|
||||
}
|
||||
if idx := strings.Index(k, annotations.MatchQuery); idx != -1 {
|
||||
key := k[start:idx] + k[idx+len(annotations.MatchQuery)+1:]
|
||||
params = append(params, [2]string{key, val})
|
||||
}
|
||||
}
|
||||
sort.SliceStable(headers, func(i, j int) bool {
|
||||
return headers[i][0] < headers[j][0]
|
||||
})
|
||||
sort.SliceStable(params, func(i, j int) bool {
|
||||
return params[i][0] < params[j][0]
|
||||
})
|
||||
for idx := range headers {
|
||||
if idx != 0 {
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
sb.WriteString(headers[idx][0])
|
||||
sb.WriteByte('\t')
|
||||
sb.WriteString(headers[idx][1])
|
||||
}
|
||||
sb.WriteString(sep)
|
||||
for idx := range params {
|
||||
if idx != 0 {
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
sb.WriteString(params[idx][0])
|
||||
sb.WriteByte('\t')
|
||||
sb.WriteString(params[idx][1])
|
||||
}
|
||||
sb.WriteString(sep)
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func transformHosts(host string) kset.String {
|
||||
hosts := []string{host}
|
||||
out := kset.NewString()
|
||||
out.Insert(hosts...)
|
||||
return out
|
||||
}
|
||||
|
||||
func isIngressPublic(ingSpec *ingress.IngressSpec) bool {
|
||||
for _, rule := range ingSpec.Rules {
|
||||
if rule.Visibility == ingress.IngressVisibilityExternalIP {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
604
pkg/ingress/kube/kingress/controller_test.go
Normal file
604
pkg/ingress/kube/kingress/controller_test.go
Normal file
@@ -0,0 +1,604 @@
|
||||
// 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 kingress
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
istiov1alpha3 "istio.io/api/networking/v1alpha3"
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pkg/config"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"knative.dev/networking/pkg/apis/networking"
|
||||
"knative.dev/networking/pkg/apis/networking/v1alpha1"
|
||||
ingress "knative.dev/networking/pkg/apis/networking/v1alpha1"
|
||||
"knative.dev/pkg/kmeta"
|
||||
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/annotations"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/common"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/secret"
|
||||
"github.com/alibaba/higress/pkg/kube"
|
||||
)
|
||||
|
||||
const (
|
||||
testNS = "testNS"
|
||||
IstioIngressClassNametest = "higress"
|
||||
)
|
||||
|
||||
var (
|
||||
ingressRules = []v1alpha1.IngressRule{{
|
||||
Hosts: []string{
|
||||
"host-tls.example.com",
|
||||
},
|
||||
HTTP: &v1alpha1.HTTPIngressRuleValue{
|
||||
Paths: []v1alpha1.HTTPIngressPath{{
|
||||
Splits: []v1alpha1.IngressBackendSplit{{
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceNamespace: testNS,
|
||||
ServiceName: "test-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
}},
|
||||
},
|
||||
Visibility: v1alpha1.IngressVisibilityExternalIP,
|
||||
}, {
|
||||
Hosts: []string{
|
||||
"host-tls.test-ns.svc.cluster.local",
|
||||
},
|
||||
HTTP: &v1alpha1.HTTPIngressRuleValue{
|
||||
Paths: []v1alpha1.HTTPIngressPath{{
|
||||
Splits: []v1alpha1.IngressBackendSplit{{
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceNamespace: testNS,
|
||||
ServiceName: "test-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
}},
|
||||
},
|
||||
Visibility: v1alpha1.IngressVisibilityClusterLocal,
|
||||
}}
|
||||
|
||||
ingressTLS = []v1alpha1.IngressTLS{{
|
||||
Hosts: []string{"host-tls.example.com"},
|
||||
SecretName: "secret0",
|
||||
SecretNamespace: "istio-system",
|
||||
}}
|
||||
|
||||
// The gateway server according to ingressTLS.
|
||||
ingressTLSServer = &istiov1alpha3.Server{
|
||||
Hosts: []string{"host-tls.example.com"},
|
||||
Port: &istiov1alpha3.Port{
|
||||
Name: "test-ns/reconciling-ingress:0",
|
||||
Number: 443,
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &istiov1alpha3.ServerTLSSettings{
|
||||
Mode: istiov1alpha3.ServerTLSSettings_SIMPLE,
|
||||
ServerCertificate: "tls.crt",
|
||||
PrivateKey: "tls.key",
|
||||
CredentialName: "secret0",
|
||||
},
|
||||
}
|
||||
|
||||
ingressHTTPServer = &istiov1alpha3.Server{
|
||||
Hosts: []string{"host-tls.example.com"},
|
||||
Port: &istiov1alpha3.Port{
|
||||
Name: "http-server",
|
||||
Number: 80,
|
||||
Protocol: "HTTP",
|
||||
},
|
||||
}
|
||||
|
||||
ingressHTTPRedirectServer = &istiov1alpha3.Server{
|
||||
Hosts: []string{"*"},
|
||||
Port: &istiov1alpha3.Port{
|
||||
Name: "http-server",
|
||||
Number: 80,
|
||||
Protocol: "HTTP",
|
||||
},
|
||||
Tls: &istiov1alpha3.ServerTLSSettings{
|
||||
HttpsRedirect: true,
|
||||
},
|
||||
}
|
||||
|
||||
// The gateway server irrelevant to ingressTLS.
|
||||
irrelevantServer = &istiov1alpha3.Server{
|
||||
Hosts: []string{"host-tls.example.com", "host-tls.test-ns.svc.cluster.local"},
|
||||
Port: &istiov1alpha3.Port{
|
||||
Name: "test:0",
|
||||
Number: 443,
|
||||
Protocol: "HTTPS",
|
||||
},
|
||||
Tls: &istiov1alpha3.ServerTLSSettings{
|
||||
Mode: istiov1alpha3.ServerTLSSettings_SIMPLE,
|
||||
ServerCertificate: "tls.crt",
|
||||
PrivateKey: "tls.key",
|
||||
CredentialName: "other-secret",
|
||||
},
|
||||
}
|
||||
irrelevantServer1 = &istiov1alpha3.Server{
|
||||
Hosts: []string{"*"},
|
||||
Port: &istiov1alpha3.Port{
|
||||
Name: "http-server",
|
||||
Number: 80,
|
||||
Protocol: "HTTP",
|
||||
},
|
||||
}
|
||||
|
||||
deletionTime = metav1.NewTime(time.Unix(1e9, 0))
|
||||
)
|
||||
|
||||
func TestKIngressControllerConventions(t *testing.T) {
|
||||
fakeClient := kube.NewFakeClient()
|
||||
localKubeClient, client := fakeClient, fakeClient
|
||||
|
||||
options := common.Options{IngressClass: "mse", ClusterId: "", EnableStatus: true}
|
||||
|
||||
secretController := secret.NewController(localKubeClient, options.ClusterId)
|
||||
ingressController := NewController(localKubeClient, client, options, secretController)
|
||||
|
||||
testcases := map[string]func(*testing.T, common.KIngressController){
|
||||
"test convert HTTPRoute": testConvertHTTPRoute,
|
||||
}
|
||||
for name, tc := range testcases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tc(t, ingressController)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testConvertHTTPRoute(t *testing.T, c common.KIngressController) {
|
||||
testcases := []struct {
|
||||
description string
|
||||
input struct {
|
||||
options *common.ConvertOptions
|
||||
wrapperConfig *common.WrapperConfig
|
||||
}
|
||||
expectNoError bool
|
||||
}{
|
||||
{
|
||||
description: "convertOptions is nil",
|
||||
input: struct {
|
||||
options *common.ConvertOptions
|
||||
wrapperConfig *common.WrapperConfig
|
||||
}{
|
||||
options: nil,
|
||||
wrapperConfig: nil,
|
||||
},
|
||||
expectNoError: false,
|
||||
}, {
|
||||
description: "convertOptions is not nil but empty",
|
||||
input: struct {
|
||||
options *common.ConvertOptions
|
||||
wrapperConfig *common.WrapperConfig
|
||||
}{
|
||||
options: &common.ConvertOptions{},
|
||||
wrapperConfig: &common.WrapperConfig{
|
||||
Config: &config.Config{},
|
||||
AnnotationsConfig: &annotations.Ingress{},
|
||||
},
|
||||
},
|
||||
expectNoError: false,
|
||||
}, {
|
||||
description: "valid httpRoute convention,invalid backend",
|
||||
input: struct {
|
||||
options *common.ConvertOptions
|
||||
wrapperConfig *common.WrapperConfig
|
||||
}{
|
||||
options: &common.ConvertOptions{
|
||||
IngressDomainCache: &common.IngressDomainCache{
|
||||
Valid: make(map[string]*common.IngressDomainBuilder),
|
||||
Invalid: make([]model.IngressDomain, 0),
|
||||
},
|
||||
Route2Ingress: map[string]*common.WrapperConfigWithRuleKey{},
|
||||
VirtualServices: make(map[string]*common.WrapperVirtualService),
|
||||
Gateways: make(map[string]*common.WrapperGateway),
|
||||
IngressRouteCache: &common.IngressRouteCache{},
|
||||
HTTPRoutes: make(map[string][]*common.WrapperHTTPRoute),
|
||||
},
|
||||
wrapperConfig: &common.WrapperConfig{Config: &config.Config{
|
||||
Spec: ingress.IngressSpec{Rules: []ingress.IngressRule{
|
||||
{
|
||||
Hosts: []string{
|
||||
"host-tls.example.com",
|
||||
},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{{
|
||||
Splits: []ingress.IngressBackendSplit{{
|
||||
IngressBackend: ingress.IngressBackend{},
|
||||
Percent: 100,
|
||||
}},
|
||||
}},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityExternalIP,
|
||||
},
|
||||
},
|
||||
TLS: []ingress.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"test1", "test2"},
|
||||
SecretName: "test",
|
||||
},
|
||||
}},
|
||||
}, AnnotationsConfig: &annotations.Ingress{},
|
||||
},
|
||||
},
|
||||
expectNoError: true,
|
||||
}, {
|
||||
description: "valid httpRoute convention,invalid split",
|
||||
input: struct {
|
||||
options *common.ConvertOptions
|
||||
wrapperConfig *common.WrapperConfig
|
||||
}{
|
||||
options: &common.ConvertOptions{
|
||||
IngressDomainCache: &common.IngressDomainCache{
|
||||
Valid: make(map[string]*common.IngressDomainBuilder),
|
||||
Invalid: make([]model.IngressDomain, 0),
|
||||
},
|
||||
Route2Ingress: map[string]*common.WrapperConfigWithRuleKey{},
|
||||
VirtualServices: make(map[string]*common.WrapperVirtualService),
|
||||
Gateways: make(map[string]*common.WrapperGateway),
|
||||
IngressRouteCache: &common.IngressRouteCache{},
|
||||
HTTPRoutes: make(map[string][]*common.WrapperHTTPRoute),
|
||||
},
|
||||
wrapperConfig: &common.WrapperConfig{Config: &config.Config{
|
||||
Spec: ingress.IngressSpec{Rules: []ingress.IngressRule{
|
||||
{
|
||||
Hosts: []string{
|
||||
"host-tls.example.com",
|
||||
},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{{
|
||||
Splits: []ingress.IngressBackendSplit{{}},
|
||||
}},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityExternalIP,
|
||||
},
|
||||
},
|
||||
TLS: []ingress.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"test1", "test2"},
|
||||
SecretName: "test",
|
||||
},
|
||||
}},
|
||||
}, AnnotationsConfig: &annotations.Ingress{},
|
||||
},
|
||||
},
|
||||
expectNoError: true,
|
||||
},
|
||||
{
|
||||
description: "valid httpRoute convention, vaild ingress",
|
||||
input: struct {
|
||||
options *common.ConvertOptions
|
||||
wrapperConfig *common.WrapperConfig
|
||||
}{
|
||||
options: &common.ConvertOptions{
|
||||
IngressDomainCache: &common.IngressDomainCache{
|
||||
Valid: make(map[string]*common.IngressDomainBuilder),
|
||||
Invalid: make([]model.IngressDomain, 0),
|
||||
},
|
||||
Route2Ingress: map[string]*common.WrapperConfigWithRuleKey{},
|
||||
VirtualServices: make(map[string]*common.WrapperVirtualService),
|
||||
Gateways: make(map[string]*common.WrapperGateway),
|
||||
IngressRouteCache: common.NewIngressRouteCache(),
|
||||
HTTPRoutes: make(map[string][]*common.WrapperHTTPRoute),
|
||||
},
|
||||
wrapperConfig: &common.WrapperConfig{Config: &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "host-tls-test",
|
||||
Namespace: testNS,
|
||||
},
|
||||
Spec: ingress.IngressSpec{Rules: []ingress.IngressRule{
|
||||
{
|
||||
Hosts: []string{
|
||||
"host-tls.example.com",
|
||||
},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{{
|
||||
Splits: []ingress.IngressBackendSplit{{
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceNamespace: testNS,
|
||||
ServiceName: "v1-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
}},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityExternalIP,
|
||||
},
|
||||
},
|
||||
TLS: []ingress.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"test1", "test2"},
|
||||
SecretName: "test",
|
||||
},
|
||||
}},
|
||||
}, AnnotationsConfig: &annotations.Ingress{},
|
||||
},
|
||||
},
|
||||
expectNoError: true,
|
||||
}, {
|
||||
description: "valid httpRoute convention, Spec Rule All open Ingress",
|
||||
input: struct {
|
||||
options *common.ConvertOptions
|
||||
wrapperConfig *common.WrapperConfig
|
||||
}{
|
||||
options: &common.ConvertOptions{
|
||||
IngressDomainCache: &common.IngressDomainCache{
|
||||
Valid: make(map[string]*common.IngressDomainBuilder),
|
||||
Invalid: make([]model.IngressDomain, 0),
|
||||
},
|
||||
Route2Ingress: map[string]*common.WrapperConfigWithRuleKey{},
|
||||
VirtualServices: make(map[string]*common.WrapperVirtualService),
|
||||
Gateways: make(map[string]*common.WrapperGateway),
|
||||
IngressRouteCache: common.NewIngressRouteCache(),
|
||||
HTTPRoutes: make(map[string][]*common.WrapperHTTPRoute),
|
||||
},
|
||||
wrapperConfig: &common.WrapperConfig{Config: &config.Config{
|
||||
Meta: config.Meta{
|
||||
Name: "host-kingress-all-open-test",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: ingress.IngressSpec{Rules: []ingress.IngressRule{
|
||||
{
|
||||
Hosts: []string{
|
||||
"hello.default",
|
||||
"hello.default.svc",
|
||||
"hello.default.svc.cluster.local",
|
||||
},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{{
|
||||
Path: "/pet/",
|
||||
Splits: []v1alpha1.IngressBackendSplit{{
|
||||
AppendHeaders: map[string]string{
|
||||
"Knative-Serving-Namespace": "default",
|
||||
"Knative-Serving-Revision": "hello-00002",
|
||||
},
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceNamespace: "default",
|
||||
ServiceName: "hello-00002",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 90,
|
||||
}, {
|
||||
AppendHeaders: map[string]string{
|
||||
"Knative-Serving-Namespace": "default",
|
||||
"Knative-Serving-Revision": "hello-00001",
|
||||
},
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceNamespace: "default",
|
||||
ServiceName: "hello-00001",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 10,
|
||||
}},
|
||||
AppendHeaders: map[string]string{
|
||||
"ugh": "blah",
|
||||
},
|
||||
}},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityClusterLocal,
|
||||
}, {
|
||||
Hosts: []string{
|
||||
"hello.default.zwj.com",
|
||||
},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{{
|
||||
Splits: []v1alpha1.IngressBackendSplit{{
|
||||
AppendHeaders: map[string]string{
|
||||
"Knative-Serving-Namespace": "default",
|
||||
"Knative-Serving-Revision": "hello-00002",
|
||||
},
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceNamespace: "default",
|
||||
ServiceName: "hello-00002",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 90,
|
||||
}, {
|
||||
AppendHeaders: map[string]string{
|
||||
"Knative-Serving-Namespace": "default",
|
||||
"Knative-Serving-Revision": "hello-00001",
|
||||
},
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceNamespace: "default",
|
||||
ServiceName: "hello-00001",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 10,
|
||||
}},
|
||||
}},
|
||||
},
|
||||
Visibility: ingress.IngressVisibilityExternalIP,
|
||||
},
|
||||
},
|
||||
TLS: []ingress.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"test1", "test2"},
|
||||
SecretName: "test",
|
||||
},
|
||||
}},
|
||||
}, AnnotationsConfig: &annotations.Ingress{},
|
||||
},
|
||||
},
|
||||
expectNoError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
err := c.ConvertHTTPRoute(testcase.input.options, testcase.input.wrapperConfig)
|
||||
if err != nil {
|
||||
require.Equal(t, testcase.expectNoError, false)
|
||||
} else {
|
||||
require.Equal(t, testcase.expectNoError, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractTLSSecretName(t *testing.T) {
|
||||
testcases := []struct {
|
||||
input struct {
|
||||
host string
|
||||
tls []ingress.IngressTLS
|
||||
}
|
||||
expect string
|
||||
description string
|
||||
}{
|
||||
{
|
||||
input: struct {
|
||||
host string
|
||||
tls []ingress.IngressTLS
|
||||
}{
|
||||
host: "",
|
||||
tls: nil,
|
||||
},
|
||||
expect: "",
|
||||
description: "both are nil",
|
||||
},
|
||||
{
|
||||
input: struct {
|
||||
host string
|
||||
tls []ingress.IngressTLS
|
||||
}{
|
||||
host: "test",
|
||||
tls: []ingress.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"test"},
|
||||
SecretName: "test-secret",
|
||||
},
|
||||
{
|
||||
Hosts: []string{"test1"},
|
||||
SecretName: "test1-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
expect: "test-secret",
|
||||
description: "found secret name",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
actual := extractTLSSecretName(testcase.input.host, testcase.input.tls)
|
||||
require.Equal(t, testcase.expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldProcessIngressUpdate(t *testing.T) {
|
||||
c := controller{
|
||||
options: common.Options{},
|
||||
ingresses: make(map[string]*ingress.Ingress),
|
||||
}
|
||||
ingress1 := &ingress.Ingress{
|
||||
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-1",
|
||||
},
|
||||
Spec: ingress.IngressSpec{
|
||||
Rules: []ingress.IngressRule{
|
||||
{
|
||||
Hosts: []string{
|
||||
"host-tls.example.com",
|
||||
},
|
||||
HTTP: &ingress.HTTPIngressRuleValue{
|
||||
Paths: []ingress.HTTPIngressPath{{
|
||||
Splits: []ingress.IngressBackendSplit{{
|
||||
IngressBackend: ingress.IngressBackend{
|
||||
ServiceNamespace: "testNs",
|
||||
ServiceName: "test-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
addAnnotations(ingress1, map[string]string{networking.IngressClassAnnotationKey: IstioIngressClassNametest})
|
||||
|
||||
should, _ := c.shouldProcessIngressUpdate(ingress1)
|
||||
if !should {
|
||||
t.Fatal("should be true")
|
||||
}
|
||||
|
||||
ingress2 := *ingress1
|
||||
should, _ = c.shouldProcessIngressUpdate(&ingress2)
|
||||
if should {
|
||||
t.Fatal("should be false")
|
||||
}
|
||||
|
||||
ingress3 := *ingress1
|
||||
ingress3.Annotations = map[string]string{
|
||||
"test": "true",
|
||||
}
|
||||
should, _ = c.shouldProcessIngressUpdate(&ingress3)
|
||||
if !should {
|
||||
t.Fatal("should be true")
|
||||
}
|
||||
ingress4 := ingress1.DeepCopy()
|
||||
addAnnotations(ingress4, map[string]string{networking.IngressClassAnnotationKey: "fake-classname"})
|
||||
should, _ = c.shouldProcessIngressUpdate(ingress4)
|
||||
if should {
|
||||
t.Fatal("should be false")
|
||||
}
|
||||
//可能有坑,annotation更新可能会引起ingress资源的反复处理。
|
||||
|
||||
}
|
||||
|
||||
func addAnnotations(ing *ingress.Ingress, annos map[string]string) *ingress.Ingress {
|
||||
// UnionMaps(a, b) where value from b wins. Use annos for second arg.
|
||||
ing.ObjectMeta.Annotations = kmeta.UnionMaps(ing.ObjectMeta.Annotations, annos)
|
||||
return ing
|
||||
}
|
||||
|
||||
func TestCreateRuleKey(t *testing.T) {
|
||||
sep := "\n\n"
|
||||
wrapperHttpRoute := &common.WrapperHTTPRoute{
|
||||
Host: "higress.com",
|
||||
OriginPathType: common.Prefix,
|
||||
OriginPath: "/foo",
|
||||
}
|
||||
|
||||
annots := annotations.Annotations{
|
||||
buildHigressAnnotationKey(annotations.MatchMethod): "GET PUT",
|
||||
buildHigressAnnotationKey("exact-" + annotations.MatchHeader + "-abc"): "123",
|
||||
buildHigressAnnotationKey("prefix-" + annotations.MatchHeader + "-def"): "456",
|
||||
buildHigressAnnotationKey("exact-" + annotations.MatchQuery + "-region"): "beijing",
|
||||
buildHigressAnnotationKey("prefix-" + annotations.MatchQuery + "-user-id"): "user-",
|
||||
}
|
||||
expect := "higress.com-prefix-/foo" + sep + //host-pathType-path
|
||||
"GET PUT" + sep + // method
|
||||
"exact-abc\t123" + "\n" + "prefix-def\t456" + sep + // header
|
||||
"exact-region\tbeijing" + "\n" + "prefix-user-id\tuser-" + sep // params
|
||||
|
||||
key := createRuleKey(annots, wrapperHttpRoute.PathFormat())
|
||||
if diff := cmp.Diff(expect, key); diff != "" {
|
||||
|
||||
t.Errorf("CreateRuleKey() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func buildHigressAnnotationKey(key string) string {
|
||||
return annotations.HigressAnnotationsPrefix + "/" + key
|
||||
}
|
||||
19
pkg/ingress/kube/kingress/resources/doc.go
Normal file
19
pkg/ingress/kube/kingress/resources/doc.go
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
Copyright 2019 The Knative Authors
|
||||
|
||||
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 resources holds simple functions for synthesizing child resources from
|
||||
// an Ingress resource and any relevant Ingress controller configuration.
|
||||
package resources
|
||||
148
pkg/ingress/kube/kingress/resources/virtual_service.go
Normal file
148
pkg/ingress/kube/kingress/resources/virtual_service.go
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
Copyright 2019 The Knative Authors
|
||||
|
||||
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 resources
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
|
||||
istiov1alpha3 "istio.io/api/networking/v1alpha3"
|
||||
"knative.dev/networking/pkg/apis/networking/v1alpha1"
|
||||
"knative.dev/pkg/network"
|
||||
)
|
||||
|
||||
func MakeVirtualServiceRoute(hosts sets.String, http *v1alpha1.HTTPIngressPath) *istiov1alpha3.HTTPRoute {
|
||||
matches := []*istiov1alpha3.HTTPMatchRequest{}
|
||||
// Deduplicate hosts to avoid excessive matches, which cause a combinatorial expansion in Istio
|
||||
|
||||
for _, host := range hosts.List() {
|
||||
matches = append(matches, makeMatch(host, http.Path, http.Headers))
|
||||
}
|
||||
|
||||
weights := []*istiov1alpha3.HTTPRouteDestination{}
|
||||
for _, split := range http.Splits {
|
||||
var h *istiov1alpha3.Headers
|
||||
if len(split.AppendHeaders) > 0 {
|
||||
h = &istiov1alpha3.Headers{
|
||||
Request: &istiov1alpha3.Headers_HeaderOperations{
|
||||
Set: split.AppendHeaders,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
weights = append(weights, &istiov1alpha3.HTTPRouteDestination{
|
||||
Destination: &istiov1alpha3.Destination{
|
||||
Host: network.GetServiceHostname(
|
||||
split.ServiceName, split.ServiceNamespace),
|
||||
Port: &istiov1alpha3.PortSelector{
|
||||
Number: uint32(split.ServicePort.IntValue()),
|
||||
},
|
||||
},
|
||||
Weight: int32(split.Percent),
|
||||
Headers: h,
|
||||
})
|
||||
}
|
||||
|
||||
var h *istiov1alpha3.Headers
|
||||
if len(http.AppendHeaders) > 0 {
|
||||
h = &istiov1alpha3.Headers{
|
||||
Request: &istiov1alpha3.Headers_HeaderOperations{
|
||||
Set: http.AppendHeaders,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var rewrite *istiov1alpha3.HTTPRewrite
|
||||
if http.RewriteHost != "" {
|
||||
rewrite = &istiov1alpha3.HTTPRewrite{
|
||||
Authority: http.RewriteHost,
|
||||
}
|
||||
}
|
||||
|
||||
route := &istiov1alpha3.HTTPRoute{
|
||||
Retries: &istiov1alpha3.HTTPRetry{}, // Override default istio behaviour of retrying twice.
|
||||
Match: matches,
|
||||
Route: weights,
|
||||
Rewrite: rewrite,
|
||||
Headers: h,
|
||||
}
|
||||
return route
|
||||
}
|
||||
|
||||
// getDistinctHostPrefixes deduplicate a set of prefix matches. For example, the set {a, aabb} can be
|
||||
// reduced to {a}, as a prefix match on {a} accepts all the same inputs as {a, aabb}.
|
||||
func getDistinctHostPrefixes(hosts sets.String) sets.String {
|
||||
// First we sort the list. This ensures that we always process the smallest elements (which match against
|
||||
// the most patterns, as they are less specific) first.
|
||||
all := hosts.List()
|
||||
ns := sets.NewString()
|
||||
for _, h := range all {
|
||||
prefixExists := false
|
||||
h = hostPrefix(h)
|
||||
// For each element, check if any existing elements are a prefix. We only insert if none are
|
||||
// // For example, if we already have {a} and we are looking at "ab", we would not add it as it has a prefix of "a"
|
||||
for e := range ns {
|
||||
if strings.HasPrefix(h, e) {
|
||||
prefixExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !prefixExists {
|
||||
ns.Insert(h)
|
||||
}
|
||||
}
|
||||
return ns
|
||||
}
|
||||
|
||||
func makeMatch(host, path string, headers map[string]v1alpha1.HeaderMatch) *istiov1alpha3.HTTPMatchRequest {
|
||||
match := &istiov1alpha3.HTTPMatchRequest{
|
||||
Authority: &istiov1alpha3.StringMatch{
|
||||
// Do not use Regex as Istio 1.4 or later has 100 bytes limitation.
|
||||
MatchType: &istiov1alpha3.StringMatch_Prefix{Prefix: host},
|
||||
},
|
||||
}
|
||||
// Empty path is considered match all path. We only need to consider path
|
||||
// when it's non-empty.
|
||||
if path != "" {
|
||||
match.Uri = &istiov1alpha3.StringMatch{
|
||||
MatchType: &istiov1alpha3.StringMatch_Prefix{Prefix: path},
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range headers {
|
||||
match.Headers = map[string]*istiov1alpha3.StringMatch{
|
||||
k: {
|
||||
MatchType: &istiov1alpha3.StringMatch_Exact{
|
||||
Exact: v.Exact,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return match
|
||||
}
|
||||
|
||||
// hostPrefix returns an host to match either host or host:<any port>.
|
||||
// For clusterLocalHost, it trims .svc.<local domain> from the host to match short host.
|
||||
func hostPrefix(host string) string {
|
||||
localDomainSuffix := ".svc." + network.GetClusterDomainName()
|
||||
if !strings.HasSuffix(host, localDomainSuffix) {
|
||||
return host
|
||||
}
|
||||
return strings.TrimSuffix(host, localDomainSuffix)
|
||||
}
|
||||
258
pkg/ingress/kube/kingress/resources/virtual_service_test.go
Normal file
258
pkg/ingress/kube/kingress/resources/virtual_service_test.go
Normal file
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
Copyright 2019 The Knative Authors.
|
||||
|
||||
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 resources
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"google.golang.org/protobuf/testing/protocmp"
|
||||
istiov1alpha3 "istio.io/api/networking/v1alpha3"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"knative.dev/networking/pkg/apis/networking/v1alpha1"
|
||||
"knative.dev/pkg/system"
|
||||
_ "knative.dev/pkg/system/testing"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultIngressRuleValue = &v1alpha1.HTTPIngressRuleValue{
|
||||
Paths: []v1alpha1.HTTPIngressPath{{
|
||||
Splits: []v1alpha1.IngressBackendSplit{{
|
||||
Percent: 100,
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceNamespace: "test",
|
||||
ServiceName: "test.svc.cluster.local",
|
||||
ServicePort: intstr.FromInt(8080),
|
||||
},
|
||||
}},
|
||||
}},
|
||||
}
|
||||
defaultIngress = v1alpha1.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-ingress",
|
||||
Namespace: system.Namespace(),
|
||||
},
|
||||
Spec: v1alpha1.IngressSpec{Rules: []v1alpha1.IngressRule{{
|
||||
Hosts: []string{
|
||||
"test-route.test-ns.svc.cluster.local",
|
||||
},
|
||||
HTTP: defaultIngressRuleValue,
|
||||
}}},
|
||||
}
|
||||
defaultVSCmpOpts = protocmp.Transform()
|
||||
)
|
||||
|
||||
func TestMakeVirtualServiceRoute_RewriteHost(t *testing.T) {
|
||||
ingressPath := &v1alpha1.HTTPIngressPath{
|
||||
RewriteHost: "the.target.host",
|
||||
Splits: []v1alpha1.IngressBackendSplit{{
|
||||
Percent: 100,
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceName: "the-svc",
|
||||
ServiceNamespace: "the-ns",
|
||||
ServicePort: intstr.FromInt(8080),
|
||||
},
|
||||
}},
|
||||
}
|
||||
route := MakeVirtualServiceRoute(sets.NewString("a.vanity.url", "another.vanity.url"), ingressPath)
|
||||
expected := &istiov1alpha3.HTTPRoute{
|
||||
Retries: &istiov1alpha3.HTTPRetry{},
|
||||
Match: []*istiov1alpha3.HTTPMatchRequest{{
|
||||
Authority: &istiov1alpha3.StringMatch{
|
||||
MatchType: &istiov1alpha3.StringMatch_Prefix{Prefix: `a.vanity.url`},
|
||||
},
|
||||
}, {
|
||||
Authority: &istiov1alpha3.StringMatch{
|
||||
MatchType: &istiov1alpha3.StringMatch_Prefix{Prefix: `another.vanity.url`},
|
||||
},
|
||||
}},
|
||||
Rewrite: &istiov1alpha3.HTTPRewrite{
|
||||
Authority: "the.target.host",
|
||||
},
|
||||
Route: []*istiov1alpha3.HTTPRouteDestination{{
|
||||
Destination: &istiov1alpha3.Destination{
|
||||
Host: "the-svc.the-ns.svc.cluster.local",
|
||||
Port: &istiov1alpha3.PortSelector{
|
||||
Number: 8080,
|
||||
},
|
||||
},
|
||||
Weight: 100,
|
||||
}},
|
||||
}
|
||||
if diff := cmp.Diff(expected, route, defaultVSCmpOpts); diff != "" {
|
||||
t.Error("Unexpected route (-want +got):", diff)
|
||||
}
|
||||
}
|
||||
|
||||
// One active target.
|
||||
func TestMakeVirtualServiceRoute_Vanilla(t *testing.T) {
|
||||
ingressPath := &v1alpha1.HTTPIngressPath{
|
||||
Headers: map[string]v1alpha1.HeaderMatch{
|
||||
"my-header": {
|
||||
Exact: "my-header-value",
|
||||
},
|
||||
},
|
||||
Splits: []v1alpha1.IngressBackendSplit{{
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceNamespace: "test-ns",
|
||||
ServiceName: "revision-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
}
|
||||
route := MakeVirtualServiceRoute(sets.NewString("a.com", "b.org"), ingressPath)
|
||||
expected := &istiov1alpha3.HTTPRoute{
|
||||
Retries: &istiov1alpha3.HTTPRetry{},
|
||||
Match: []*istiov1alpha3.HTTPMatchRequest{{
|
||||
Authority: &istiov1alpha3.StringMatch{
|
||||
MatchType: &istiov1alpha3.StringMatch_Prefix{Prefix: `a.com`},
|
||||
},
|
||||
Headers: map[string]*istiov1alpha3.StringMatch{
|
||||
"my-header": {
|
||||
MatchType: &istiov1alpha3.StringMatch_Exact{
|
||||
Exact: "my-header-value",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
Authority: &istiov1alpha3.StringMatch{
|
||||
MatchType: &istiov1alpha3.StringMatch_Prefix{Prefix: `b.org`},
|
||||
},
|
||||
Headers: map[string]*istiov1alpha3.StringMatch{
|
||||
"my-header": {
|
||||
MatchType: &istiov1alpha3.StringMatch_Exact{
|
||||
Exact: "my-header-value",
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
Route: []*istiov1alpha3.HTTPRouteDestination{{
|
||||
Destination: &istiov1alpha3.Destination{
|
||||
Host: "revision-service.test-ns.svc.cluster.local",
|
||||
Port: &istiov1alpha3.PortSelector{Number: 80},
|
||||
},
|
||||
Weight: 100,
|
||||
}},
|
||||
}
|
||||
if diff := cmp.Diff(expected, route, defaultVSCmpOpts); diff != "" {
|
||||
t.Error("Unexpected route (-want +got):", diff)
|
||||
}
|
||||
}
|
||||
|
||||
// One active target.
|
||||
func TestMakeVirtualServiceRoute_Internal(t *testing.T) {
|
||||
ingressPath := &v1alpha1.HTTPIngressPath{
|
||||
Splits: []v1alpha1.IngressBackendSplit{{
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceNamespace: "test-ns",
|
||||
ServiceName: "revision-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 100,
|
||||
}},
|
||||
}
|
||||
route := MakeVirtualServiceRoute(sets.NewString("a.default"), ingressPath)
|
||||
expected := &istiov1alpha3.HTTPRoute{
|
||||
Retries: &istiov1alpha3.HTTPRetry{},
|
||||
Match: []*istiov1alpha3.HTTPMatchRequest{{
|
||||
Authority: &istiov1alpha3.StringMatch{
|
||||
MatchType: &istiov1alpha3.StringMatch_Prefix{Prefix: `a.default`},
|
||||
},
|
||||
}},
|
||||
Route: []*istiov1alpha3.HTTPRouteDestination{{
|
||||
Destination: &istiov1alpha3.Destination{
|
||||
Host: "revision-service.test-ns.svc.cluster.local",
|
||||
Port: &istiov1alpha3.PortSelector{Number: 80},
|
||||
},
|
||||
Weight: 100,
|
||||
}},
|
||||
}
|
||||
if diff := cmp.Diff(expected, route, defaultVSCmpOpts); diff != "" {
|
||||
t.Error("Unexpected route (-want +got):", diff)
|
||||
}
|
||||
}
|
||||
|
||||
// Two active targets.
|
||||
func TestMakeVirtualServiceRoute_TwoTargets(t *testing.T) {
|
||||
ingressPath := &v1alpha1.HTTPIngressPath{
|
||||
Splits: []v1alpha1.IngressBackendSplit{{
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceNamespace: "test-ns",
|
||||
ServiceName: "revision-service",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
Percent: 90,
|
||||
}, {
|
||||
IngressBackend: v1alpha1.IngressBackend{
|
||||
ServiceNamespace: "test-ns",
|
||||
ServiceName: "new-revision-service",
|
||||
ServicePort: intstr.FromInt(81),
|
||||
},
|
||||
Percent: 10,
|
||||
}},
|
||||
}
|
||||
route := MakeVirtualServiceRoute(sets.NewString("test.org"), ingressPath)
|
||||
expected := &istiov1alpha3.HTTPRoute{
|
||||
Retries: &istiov1alpha3.HTTPRetry{},
|
||||
Match: []*istiov1alpha3.HTTPMatchRequest{{
|
||||
Authority: &istiov1alpha3.StringMatch{
|
||||
MatchType: &istiov1alpha3.StringMatch_Prefix{Prefix: `test.org`},
|
||||
},
|
||||
}},
|
||||
Route: []*istiov1alpha3.HTTPRouteDestination{{
|
||||
Destination: &istiov1alpha3.Destination{
|
||||
Host: "revision-service.test-ns.svc.cluster.local",
|
||||
Port: &istiov1alpha3.PortSelector{Number: 80},
|
||||
},
|
||||
Weight: 90,
|
||||
}, {
|
||||
Destination: &istiov1alpha3.Destination{
|
||||
Host: "new-revision-service.test-ns.svc.cluster.local",
|
||||
Port: &istiov1alpha3.PortSelector{Number: 81},
|
||||
},
|
||||
Weight: 10,
|
||||
}},
|
||||
}
|
||||
if diff := cmp.Diff(expected, route, defaultVSCmpOpts); diff != "" {
|
||||
t.Error("Unexpected route (-want +got):", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDistinctHostPrefixes(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in sets.String
|
||||
out sets.String
|
||||
}{
|
||||
{"empty", sets.NewString(), sets.NewString()},
|
||||
{"single element", sets.NewString("a"), sets.NewString("a")},
|
||||
{"no overlap", sets.NewString("a", "b"), sets.NewString("a", "b")},
|
||||
{"overlap", sets.NewString("a", "ab", "abc"), sets.NewString("a")},
|
||||
{"multiple overlaps", sets.NewString("a", "ab", "abc", "xyz", "xy", "m"), sets.NewString("a", "xy", "m")},
|
||||
}
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := getDistinctHostPrefixes(tt.in)
|
||||
if !tt.out.Equal(got) {
|
||||
t.Fatalf("Expected %v, got %v", tt.out, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
127
pkg/ingress/kube/kingress/status.go
Normal file
127
pkg/ingress/kube/kingress/status.go
Normal file
@@ -0,0 +1,127 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package kingress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
coreV1 "k8s.io/api/core/v1"
|
||||
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
listerv1 "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"knative.dev/networking/pkg/apis/networking/v1alpha1"
|
||||
kingressclient "knative.dev/networking/pkg/client/clientset/versioned"
|
||||
kingresslister "knative.dev/networking/pkg/client/listers/networking/v1alpha1"
|
||||
|
||||
common2 "github.com/alibaba/higress/pkg/ingress/kube/common"
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
"github.com/alibaba/higress/pkg/kube"
|
||||
)
|
||||
|
||||
// statusSyncer keeps the status IP in each Ingress resource updated
|
||||
type statusSyncer struct {
|
||||
client kingressclient.Interface
|
||||
controller *controller
|
||||
watchedNamespace string
|
||||
ingressLister kingresslister.IngressLister
|
||||
serviceLister listerv1.ServiceLister
|
||||
}
|
||||
|
||||
// newStatusSyncer creates a new instance
|
||||
func newStatusSyncer(localKubeClient, client kube.Client, controller *controller, namespace string) *statusSyncer {
|
||||
return &statusSyncer{
|
||||
client: client.KIngress(),
|
||||
controller: controller,
|
||||
watchedNamespace: namespace,
|
||||
ingressLister: client.KIngressInformer().Networking().V1alpha1().Ingresses().Lister(),
|
||||
serviceLister: localKubeClient.KubeInformer().Core().V1().Services().Lister(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *statusSyncer) run(stopCh <-chan struct{}) {
|
||||
cache.WaitForCacheSync(stopCh, s.controller.HasSynced)
|
||||
|
||||
ticker := time.NewTicker(common2.DefaultStatusUpdateInterval)
|
||||
for {
|
||||
select {
|
||||
case <-stopCh:
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := s.runUpdateStatus(); err != nil {
|
||||
IngressLog.Errorf("update status task fail, err %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *statusSyncer) runUpdateStatus() error {
|
||||
svcList, err := s.serviceLister.Services(s.watchedNamespace).List(common2.SvcLabelSelector)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
IngressLog.Debugf("found number %d of svc", len(svcList))
|
||||
lbStatusList := common2.GetLbStatusList(svcList)
|
||||
return s.updateStatus(lbStatusList)
|
||||
}
|
||||
|
||||
func transportLoadBalancerIngress(status []coreV1.LoadBalancerIngress) []v1alpha1.LoadBalancerIngressStatus {
|
||||
var KnativeLBIngress []v1alpha1.LoadBalancerIngressStatus
|
||||
for _, addr := range status {
|
||||
KnativeIng := v1alpha1.LoadBalancerIngressStatus{
|
||||
IP: addr.IP,
|
||||
Domain: addr.Hostname,
|
||||
}
|
||||
KnativeLBIngress = append(KnativeLBIngress, KnativeIng)
|
||||
}
|
||||
return KnativeLBIngress
|
||||
}
|
||||
|
||||
// updateStatus updates ingress status with the list of IP
|
||||
func (s *statusSyncer) updateStatus(status []coreV1.LoadBalancerIngress) error {
|
||||
ingressList, err := s.ingressLister.List(labels.Everything())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ingress := range ingressList {
|
||||
shouldTarget, err := s.controller.shouldProcessIngress(ingress)
|
||||
if err != nil {
|
||||
IngressLog.Warnf("error determining whether should target ingress %s/%s within cluster %s for status update: %v",
|
||||
ingress.Namespace, ingress.Name, s.controller.options.ClusterId, err)
|
||||
return err
|
||||
}
|
||||
if !shouldTarget {
|
||||
continue
|
||||
}
|
||||
ingress.Status.MarkNetworkConfigured()
|
||||
KIngressStatus := transportLoadBalancerIngress(status)
|
||||
if ingress.Status.PublicLoadBalancer == nil || len(ingress.Status.PublicLoadBalancer.Ingress) != len(KIngressStatus) || reflect.DeepEqual(ingress.Status.PublicLoadBalancer.Ingress, KIngressStatus) {
|
||||
ingress.Status.ObservedGeneration = ingress.Generation
|
||||
ingress.Status.MarkLoadBalancerReady(KIngressStatus, KIngressStatus)
|
||||
IngressLog.Infof("Update Ingress %v/%v within cluster %s status", ingress.Namespace, ingress.Name, s.controller.options.ClusterId)
|
||||
}
|
||||
_, err = s.client.NetworkingV1alpha1().Ingresses(ingress.Namespace).UpdateStatus(context.TODO(), ingress, metaV1.UpdateOptions{})
|
||||
if err != nil {
|
||||
IngressLog.Warnf("error updating ingress %s/%s within cluster %s status: %v",
|
||||
ingress.Namespace, ingress.Name, s.controller.options.ClusterId, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
204
pkg/ingress/translation/translation.go
Normal file
204
pkg/ingress/translation/translation.go
Normal file
@@ -0,0 +1,204 @@
|
||||
// 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 translation
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"istio.io/istio/pilot/pkg/model"
|
||||
"istio.io/istio/pkg/config"
|
||||
"istio.io/istio/pkg/config/schema/collection"
|
||||
"istio.io/istio/pkg/config/schema/gvk"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
ingressconfig "github.com/alibaba/higress/pkg/ingress/config"
|
||||
"github.com/alibaba/higress/pkg/ingress/kube/common"
|
||||
. "github.com/alibaba/higress/pkg/ingress/log"
|
||||
"github.com/alibaba/higress/pkg/kube"
|
||||
)
|
||||
|
||||
var (
|
||||
_ model.ConfigStoreCache = &IngressTranslation{}
|
||||
_ model.IngressStore = &IngressTranslation{}
|
||||
)
|
||||
|
||||
type IngressTranslation struct {
|
||||
ingressConfig *ingressconfig.IngressConfig
|
||||
kingressConfig *ingressconfig.KIngressConfig
|
||||
mutex sync.RWMutex
|
||||
higressRouteCache model.IngressRouteCollection
|
||||
higressDomainCache model.IngressDomainCollection
|
||||
}
|
||||
|
||||
func NewIngressTranslation(localKubeClient kube.Client, XDSUpdater model.XDSUpdater, namespace, clusterId string) *IngressTranslation {
|
||||
if clusterId == "Kubernetes" {
|
||||
clusterId = ""
|
||||
}
|
||||
Config := &IngressTranslation{
|
||||
ingressConfig: ingressconfig.NewIngressConfig(localKubeClient, XDSUpdater, namespace, clusterId),
|
||||
kingressConfig: ingressconfig.NewKIngressConfig(localKubeClient, XDSUpdater, namespace, clusterId),
|
||||
}
|
||||
return Config
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) AddLocalCluster(options common.Options) (common.IngressController, common.KIngressController) {
|
||||
if m.kingressConfig == nil {
|
||||
return m.ingressConfig.AddLocalCluster(options), nil
|
||||
}
|
||||
return m.ingressConfig.AddLocalCluster(options), m.kingressConfig.AddLocalCluster(options)
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) InitializeCluster(ingressController common.IngressController, kingressController common.KIngressController, stop <-chan struct{}) error {
|
||||
if err := m.ingressConfig.InitializeCluster(ingressController, stop); err != nil {
|
||||
return err
|
||||
}
|
||||
if kingressController == nil {
|
||||
return nil
|
||||
}
|
||||
if err := m.kingressConfig.InitializeCluster(kingressController, stop); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) RegisterEventHandler(kind config.GroupVersionKind, f model.EventHandler) {
|
||||
m.ingressConfig.RegisterEventHandler(kind, f)
|
||||
if m.kingressConfig != nil {
|
||||
m.kingressConfig.RegisterEventHandler(kind, f)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) HasSynced() bool {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
if !m.ingressConfig.HasSynced() {
|
||||
return false
|
||||
}
|
||||
if m.kingressConfig != nil {
|
||||
if !m.kingressConfig.HasSynced() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) Run(stop <-chan struct{}) {
|
||||
go m.ingressConfig.Run(stop)
|
||||
if m.kingressConfig != nil {
|
||||
go m.kingressConfig.Run(stop)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) SetWatchErrorHandler(f func(r *cache.Reflector, err error)) error {
|
||||
m.ingressConfig.SetWatchErrorHandler(f)
|
||||
if m.kingressConfig != nil {
|
||||
m.kingressConfig.SetWatchErrorHandler(f)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) GetIngressRoutes() model.IngressRouteCollection {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
ingressRouteCache := m.ingressConfig.GetIngressRoutes()
|
||||
m.higressRouteCache = model.IngressRouteCollection{}
|
||||
m.higressRouteCache.Invalid = append(m.higressRouteCache.Invalid, ingressRouteCache.Invalid...)
|
||||
m.higressRouteCache.Valid = append(m.higressRouteCache.Valid, ingressRouteCache.Valid...)
|
||||
if m.kingressConfig != nil {
|
||||
kingressRouteCache := m.kingressConfig.GetIngressRoutes()
|
||||
m.higressRouteCache.Invalid = append(m.higressRouteCache.Invalid, kingressRouteCache.Invalid...)
|
||||
m.higressRouteCache.Valid = append(m.higressRouteCache.Valid, kingressRouteCache.Valid...)
|
||||
}
|
||||
|
||||
return m.higressRouteCache
|
||||
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) GetIngressDomains() model.IngressDomainCollection {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
ingressDomainCache := m.ingressConfig.GetIngressDomains()
|
||||
|
||||
m.higressDomainCache = model.IngressDomainCollection{}
|
||||
m.higressDomainCache.Invalid = append(m.higressDomainCache.Invalid, ingressDomainCache.Invalid...)
|
||||
m.higressDomainCache.Valid = append(m.higressDomainCache.Valid, ingressDomainCache.Valid...)
|
||||
if m.kingressConfig != nil {
|
||||
kingressDomainCache := m.kingressConfig.GetIngressDomains()
|
||||
m.higressDomainCache.Invalid = append(m.higressDomainCache.Invalid, kingressDomainCache.Invalid...)
|
||||
m.higressDomainCache.Valid = append(m.higressDomainCache.Valid, kingressDomainCache.Valid...)
|
||||
}
|
||||
return m.higressDomainCache
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) Schemas() collection.Schemas {
|
||||
return common.IngressIR
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) Get(typ config.GroupVersionKind, name, namespace string) *config.Config {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) List(typ config.GroupVersionKind, namespace string) ([]config.Config, error) {
|
||||
if typ != gvk.Gateway &&
|
||||
typ != gvk.VirtualService &&
|
||||
typ != gvk.DestinationRule &&
|
||||
typ != gvk.EnvoyFilter &&
|
||||
typ != gvk.ServiceEntry &&
|
||||
typ != gvk.WasmPlugin {
|
||||
return nil, common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
// Currently, only support list all namespaces gateways or virtualservices.
|
||||
if namespace != "" {
|
||||
IngressLog.Warnf("ingress store only support type %s of all namespace.", typ)
|
||||
return nil, common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
ingressConfig, err := m.ingressConfig.List(typ, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var higressConfig []config.Config
|
||||
higressConfig = append(higressConfig, ingressConfig...)
|
||||
if m.kingressConfig != nil {
|
||||
kingressConfig, err := m.kingressConfig.List(typ, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
higressConfig = append(higressConfig, kingressConfig...)
|
||||
}
|
||||
return higressConfig, nil
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) Create(config config.Config) (revision string, err error) {
|
||||
return "", common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) Update(config config.Config) (newRevision string, err error) {
|
||||
return "", common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) UpdateStatus(config config.Config) (newRevision string, err error) {
|
||||
return "", common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) Patch(orig config.Config, patchFn config.PatchFunc) (string, error) {
|
||||
return "", common.ErrUnsupportedOp
|
||||
}
|
||||
|
||||
func (m *IngressTranslation) Delete(typ config.GroupVersionKind, name, namespace string, resourceVersion *string) error {
|
||||
return common.ErrUnsupportedOp
|
||||
}
|
||||
@@ -15,21 +15,29 @@
|
||||
package kube
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
istiokube "istio.io/istio/pkg/kube"
|
||||
apiExtensionsV1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
|
||||
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/rest"
|
||||
clienttesting "k8s.io/client-go/testing"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
kingressclient "knative.dev/networking/pkg/client/clientset/versioned"
|
||||
kingressfake "knative.dev/networking/pkg/client/clientset/versioned/fake"
|
||||
kingressinformer "knative.dev/networking/pkg/client/informers/externalversions"
|
||||
|
||||
higressclient "github.com/alibaba/higress/client/pkg/clientset/versioned"
|
||||
higressfake "github.com/alibaba/higress/client/pkg/clientset/versioned/fake"
|
||||
higressinformer "github.com/alibaba/higress/client/pkg/informers/externalversions"
|
||||
"github.com/alibaba/higress/pkg/config/constants"
|
||||
)
|
||||
|
||||
type Client interface {
|
||||
@@ -40,6 +48,11 @@ type Client interface {
|
||||
|
||||
// HigressInformer returns an informer for the higress client
|
||||
HigressInformer() higressinformer.SharedInformerFactory
|
||||
|
||||
//KIngress return the Knative kube client
|
||||
KIngress() kingressclient.Interface
|
||||
|
||||
KIngressInformer() kingressinformer.SharedInformerFactory
|
||||
}
|
||||
|
||||
type client struct {
|
||||
@@ -48,9 +61,12 @@ type client struct {
|
||||
higress higressclient.Interface
|
||||
higressInformer higressinformer.SharedInformerFactory
|
||||
|
||||
kingress kingressclient.Interface
|
||||
kingressInformer kingressinformer.SharedInformerFactory
|
||||
// If enable, will wait for cache syncs with extremely short delay. This should be used only for tests
|
||||
fastSync bool
|
||||
informerWatchesPending *atomic.Int32
|
||||
fastSync bool
|
||||
informerWatchesPending *atomic.Int32
|
||||
kinformerWatchesPending *atomic.Int32
|
||||
}
|
||||
|
||||
const resyncInterval = 0
|
||||
@@ -62,7 +78,9 @@ func NewFakeClient(objects ...runtime.Object) Client {
|
||||
c.higress = higressfake.NewSimpleClientset()
|
||||
c.higressInformer = higressinformer.NewSharedInformerFactoryWithOptions(c.higress, resyncInterval)
|
||||
c.informerWatchesPending = atomic.NewInt32(0)
|
||||
|
||||
c.kingress = kingressfake.NewSimpleClientset()
|
||||
c.kingressInformer = kingressinformer.NewSharedInformerFactoryWithOptions(c.kingress, resyncInterval)
|
||||
c.kinformerWatchesPending = atomic.NewInt32(0)
|
||||
// https://github.com/kubernetes/kubernetes/issues/95372
|
||||
// There is a race condition in the client fakes, where events that happen between the List and Watch
|
||||
// of an informer are dropped. To avoid this, we explicitly manage the list and watch, ensuring all lists
|
||||
@@ -90,6 +108,27 @@ func NewFakeClient(objects ...runtime.Object) Client {
|
||||
fc := c.higress.(*higressfake.Clientset)
|
||||
fc.PrependReactor("list", "&", listReactor)
|
||||
fc.PrependWatchReactor("*", watchReactor(fc.Tracker()))
|
||||
|
||||
klistReactor := func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
c.kinformerWatchesPending.Inc()
|
||||
return false, nil, nil
|
||||
}
|
||||
kwatchReactor := func(tracker clienttesting.ObjectTracker) func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
|
||||
return func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
|
||||
gvr := action.GetResource()
|
||||
ns := action.GetNamespace()
|
||||
watch, err := tracker.Watch(gvr, ns)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
c.kinformerWatchesPending.Dec()
|
||||
return true, watch, nil
|
||||
}
|
||||
}
|
||||
fcknative := c.kingress.(*kingressfake.Clientset)
|
||||
fcknative.PrependReactor("list", "&", klistReactor)
|
||||
fcknative.PrependWatchReactor("*", kwatchReactor(fcknative.Tracker()))
|
||||
|
||||
c.fastSync = true
|
||||
return c
|
||||
}
|
||||
@@ -107,9 +146,28 @@ func NewClient(clientConfig clientcmd.ClientConfig) (Client, error) {
|
||||
return nil, err
|
||||
}
|
||||
c.higressInformer = higressinformer.NewSharedInformerFactory(c.higress, resyncInterval)
|
||||
|
||||
c.kingress, err = kingressclient.NewForConfig(istioClient.RESTConfig())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if CheckKIngressCRDExist(istioClient.RESTConfig()) {
|
||||
c.kingressInformer = kingressinformer.NewSharedInformerFactory(c.kingress, resyncInterval)
|
||||
} else {
|
||||
c.kingressInformer = nil
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func (c *client) KIngress() kingressclient.Interface {
|
||||
return c.kingress
|
||||
}
|
||||
|
||||
func (c *client) KIngressInformer() kingressinformer.SharedInformerFactory {
|
||||
return c.kingressInformer
|
||||
}
|
||||
|
||||
func (c *client) Higress() higressclient.Interface {
|
||||
return c.higress
|
||||
}
|
||||
@@ -121,6 +179,7 @@ func (c *client) HigressInformer() higressinformer.SharedInformerFactory {
|
||||
func (c *client) RunAndWait(stop <-chan struct{}) {
|
||||
c.Client.RunAndWait(stop)
|
||||
c.higressInformer.Start(stop)
|
||||
|
||||
if c.fastSync {
|
||||
fastWaitForCacheSync(stop, c.higressInformer)
|
||||
_ = wait.PollImmediate(time.Microsecond*100, wait.ForeverTestTimeout, func() (bool, error) {
|
||||
@@ -137,6 +196,27 @@ func (c *client) RunAndWait(stop <-chan struct{}) {
|
||||
} else {
|
||||
c.higressInformer.WaitForCacheSync(stop)
|
||||
}
|
||||
|
||||
if c.kingressInformer != nil {
|
||||
c.kingressInformer.Start(stop)
|
||||
if c.fastSync {
|
||||
fastWaitForCacheSync(stop, c.kingressInformer)
|
||||
_ = wait.PollImmediate(time.Microsecond*100, wait.ForeverTestTimeout, func() (bool, error) {
|
||||
select {
|
||||
case <-stop:
|
||||
return false, fmt.Errorf("channel closed")
|
||||
default:
|
||||
}
|
||||
if c.informerWatchesPending.Load() == 0 {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
} else {
|
||||
c.kingressInformer.WaitForCacheSync(stop)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type reflectInformerSync interface {
|
||||
@@ -162,3 +242,23 @@ func fastWaitForCacheSync(stop <-chan struct{}, informerFactory reflectInformerS
|
||||
return true, nil
|
||||
})
|
||||
}
|
||||
|
||||
// Check Knative Ingress CRD
|
||||
func CheckKIngressCRDExist(config *rest.Config) bool {
|
||||
apiExtClientset, err := apiExtensionsV1.NewForConfig(config)
|
||||
if err != nil {
|
||||
fmt.Errorf("failed creating apiExtension Client: %v", err)
|
||||
return false
|
||||
}
|
||||
crdList, err := apiExtClientset.CustomResourceDefinitions().List(context.TODO(), metaV1.ListOptions{})
|
||||
if err != nil {
|
||||
fmt.Errorf("failed listing Custom Resource Definition: %v", err)
|
||||
return false
|
||||
}
|
||||
for _, crd := range crdList.Items {
|
||||
if crd.Name == constants.KnativeIngressCRDName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ $ PLUGIN_NAME=request_block make build
|
||||
<details>
|
||||
<summary>Output</summary>
|
||||
<pre><code>
|
||||
|
||||
DOCKER_BUILDKIT=1 docker build --build-arg PLUGIN_NAME=request_block \
|
||||
-t request_block:20230721-141120-aa17e95 \
|
||||
--output extensions/request_block \
|
||||
@@ -106,16 +107,17 @@ spec:
|
||||
|
||||
The rules will be matched in the order of configuration. If one match is found, it will stop, and the matching configuration will take effect.
|
||||
|
||||
|
||||
## E2E test
|
||||
|
||||
When you complete a GO plug-in function, you can create associated e2e test cases at the same time, and complete the test verification of the plug-in function locally.
|
||||
|
||||
### step1. write test cases
|
||||
|
||||
In the directory of `./ test/e2e/conformance/tests/`, add the xxx.yaml file and xxx.go file. Such as test for `request-block` wasm-plugin,
|
||||
|
||||
./test/e2e/conformance/tests/request-block.yaml
|
||||
```
|
||||
|
||||
``` yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
...
|
||||
@@ -126,16 +128,18 @@ spec:
|
||||
- "swagger.html"
|
||||
url: file:///opt/plugins/wasm-go/extensions/request-block/plugin.wasm
|
||||
```
|
||||
|
||||
`Above of the url, the name of after extensions indicates the name of the folder where the plug-in resides.`
|
||||
|
||||
./test/e2e/conformance/tests/request-block.go
|
||||
|
||||
### step2. add test cases
|
||||
|
||||
Add the test cases written above to the e2e test list,
|
||||
|
||||
./test/e2e/e2e_test.go
|
||||
|
||||
```
|
||||
```go
|
||||
...
|
||||
cSuite.Setup(t)
|
||||
var higressTests []suite.ConformanceTest
|
||||
@@ -160,8 +164,9 @@ cSuite.Setup(t)
|
||||
```
|
||||
|
||||
### step3. compile and run test cases
|
||||
|
||||
Considering that building wasm locally is time-consuming, we support building only the plug-ins that need to be tested (at the same time, you can also temporarily modify the list of test cases in the second small step above, and only execute your newly written cases).
|
||||
|
||||
```bash
|
||||
PLUGIN_TYPE=CPP PLUGIN_NAME=request_block make higress-wasmplugin-test
|
||||
```
|
||||
```
|
||||
|
||||
114
plugins/wasm-go/extensions/basic-auth/README.md
Normal file
114
plugins/wasm-go/extensions/basic-auth/README.md
Normal file
@@ -0,0 +1,114 @@
|
||||
---
|
||||
title: Basic 认证
|
||||
keywords: [higress,basic auth]
|
||||
description: Basic 认证插件配置参考
|
||||
---
|
||||
|
||||
## 功能说明
|
||||
`basic-auth`插件实现了基于 HTTP Basic Auth 标准进行认证鉴权的功能
|
||||
|
||||
## 运行属性
|
||||
|
||||
插件执行阶段:`认证阶段`
|
||||
插件执行优先级:`320`
|
||||
|
||||
## 配置字段
|
||||
|
||||
### 全局配置
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ----------- | --------------- | -------- | ------ | ---------------------------------------------------- |
|
||||
| `consumers` | array of object | 必填 | - | 配置服务的调用者,用于对请求进行认证 |
|
||||
| `global_auth` | bool | 选填 | - | 若配置为true,则全局生效认证机制; 若配置为false,则只对做了配置的域名和路由生效认证机制; 若不配置则仅当没有域名和路由配置时全局生效(兼容机制) |
|
||||
|
||||
`consumers`中每一项的配置字段说明如下:
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ------------ | -------- | -------- | ------ | ------------------------ |
|
||||
| `credential` | string | 必填 | - | 配置该consumer的访问凭证 |
|
||||
| `name` | string | 必填 | - | 配置该consumer的名称 |
|
||||
|
||||
### 域名和路由级配置
|
||||
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| ---------------- | --------------- | ------------------------------------------------- | ------ | -------------------------------------------------- |
|
||||
| `allow` | array of string | 必填 | - | 对于符合匹配条件的请求,配置允许访问的consumer名称 |
|
||||
|
||||
**注意:**
|
||||
- 对于通过认证鉴权的请求,请求的header会被添加一个`X-Mse-Consumer`字段,用以标识调用者的名称。
|
||||
|
||||
## 配置示例
|
||||
|
||||
### 对特定路由或域名开启认证和鉴权
|
||||
|
||||
以下配置将对网关特定路由或域名开启 Basic Auth 认证和鉴权,注意凭证信息中的用户名和密码之间使用":"分隔,`credential`字段不能重复
|
||||
|
||||
**全局配置**
|
||||
|
||||
```yaml
|
||||
consumers:
|
||||
- credential: 'admin:123456'
|
||||
name: consumer1
|
||||
- credential: 'guest:abc'
|
||||
name: consumer2
|
||||
global_auth: false
|
||||
```
|
||||
|
||||
|
||||
**路由级配置**
|
||||
|
||||
对 route-a 和 route-b 这两个路由做如下配置:
|
||||
|
||||
```yaml
|
||||
allow:
|
||||
- consumer1
|
||||
```
|
||||
|
||||
对 *.example.com 和 test.com 在这两个域名做如下配置:
|
||||
|
||||
```yaml
|
||||
allow:
|
||||
- consumer2
|
||||
```
|
||||
|
||||
若是在控制台进行配置,此例指定的 `route-a` 和 `route-b` 即在控制台创建路由时填写的路由名称,当匹配到这两个路由时,将允许`name`为`consumer1`的调用者访问,其他调用者不允许访问;
|
||||
|
||||
此例指定的 `*.example.com` 和 `test.com` 用于匹配请求的域名,当发现域名匹配时,将允许`name`为`consumer2`的调用者访问,其他调用者不允许访问。
|
||||
|
||||
#### 根据该配置,下列请求可以允许访问:
|
||||
|
||||
**请求指定用户名密码**
|
||||
|
||||
```bash
|
||||
# 假设以下请求将会匹配到route-a路由
|
||||
# 使用 curl 的 -u 参数指定
|
||||
curl -u admin:123456 http://xxx.hello.com/test
|
||||
# 或者直接指定 Authorization 请求头,用户名密码使用 base64 编码
|
||||
curl -H 'Authorization: Basic YWRtaW46MTIzNDU2' http://xxx.hello.com/test
|
||||
```
|
||||
|
||||
认证鉴权通过后,请求的header中会被添加一个`X-Mse-Consumer`字段,在此例中其值为`consumer1`,用以标识调用方的名称
|
||||
|
||||
#### 下列请求将拒绝访问:
|
||||
|
||||
**请求未提供用户名密码,返回401**
|
||||
```bash
|
||||
curl http://xxx.hello.com/test
|
||||
```
|
||||
**请求提供的用户名密码错误,返回401**
|
||||
```bash
|
||||
curl -u admin:abc http://xxx.hello.com/test
|
||||
```
|
||||
**根据请求的用户名和密码匹配到的调用者无访问权限,返回403**
|
||||
```bash
|
||||
# consumer2不在route-a的allow列表里
|
||||
curl -u guest:abc http://xxx.hello.com/test
|
||||
```
|
||||
|
||||
## 相关错误码
|
||||
|
||||
| HTTP 状态码 | 出错信息 | 原因说明 |
|
||||
| ----------- |--------------------------------------------------------------------------------| ---------------------- |
|
||||
| 401 | Request denied by Basic Auth check. No Basic Authentication information found. | 请求未提供凭证 |
|
||||
| 401 | Request denied by Basic Auth check. Invalid username and/or password. | 请求凭证无效 |
|
||||
| 403 | Request denied by Basic Auth check. Unauthorized consumer. | 请求的调用方无访问权限 |
|
||||
119
plugins/wasm-go/extensions/basic-auth/README_EN.md
Normal file
119
plugins/wasm-go/extensions/basic-auth/README_EN.md
Normal file
@@ -0,0 +1,119 @@
|
||||
---
|
||||
title: Basic Auth
|
||||
keywords: [higress, basic auth]
|
||||
description: Basic authentication plug-in configuration reference
|
||||
---
|
||||
|
||||
## Description
|
||||
`basic-auth` plugin implements the function of authentication based on the HTTP Basic Auth standard.
|
||||
|
||||
## Configuration Fields
|
||||
|
||||
| Name | Type | Requirement | Default Value | Description |
|
||||
| ----------- | --------------- | -------- | ------ | ---------------------------------------------------- |
|
||||
| `consumers` | array of object | Required | - | Caller of the service for authentication of requests |
|
||||
| `_rules_` | array of object | Optional | - | Configure access permission list for specific routes or domains to authenticate requests |
|
||||
|
||||
Filed descriptions of `consumers` items:
|
||||
|
||||
| Name | Type | Requirement | Default Value | Description |
|
||||
| ------------ | ------ | ----------- | ------------- | ------------------------------------- |
|
||||
| `credential` | string | Required | - | Credential for this consumer's access |
|
||||
| `name` | string | Required | - | Name of this consumer |
|
||||
|
||||
Configuration field descriptions for each item in `_rules_` are as follows:
|
||||
|
||||
| Field Name | Data Type | Requirement | Default | Description |
|
||||
| ---------------- | --------------- | ------------------------------------------------- | ------ | -------------------------------------------------- |
|
||||
| `_match_route_` | array of string | One of `_match_route_` or `_match_domain_` | - | Configure the routes to match for request authorization |
|
||||
| `_match_domain_` | array of string | One of `_match_route_` , `_match_domain_` | - | Configure the domains to match for request authorization |
|
||||
| `allow` | array of string | Required | - | Configure the consumer names allowed to access requests that match the match condition |
|
||||
|
||||
**Note:**
|
||||
|
||||
- If the `_rules_` field is not configured, authentication is enabled for all routes of the current gateway instance by default;
|
||||
- For authenticated requests, `X-Mse-Consumer` field will be added to the request header to identify the name of the caller.
|
||||
|
||||
## Configuration Samples
|
||||
|
||||
### Enable Authentication and Authorization for specific routes or domains
|
||||
|
||||
The following configuration will enable Basic Auth authentication and authorization for specific routes or domains of the gateway. Note that the username and password in the credential information are separated by a ":", and the `credential` field cannot be repeated.
|
||||
|
||||
|
||||
|
||||
```yaml
|
||||
# use the _rules_ field for fine-grained rule configuration.
|
||||
consumers:
|
||||
- credential: 'admin:123456'
|
||||
name: consumer1
|
||||
- credential: 'guest:abc'
|
||||
name: consumer2
|
||||
_rules_:
|
||||
# rule 1: match by the route name.
|
||||
- _match_route_:
|
||||
- route-a
|
||||
- route-b
|
||||
allow:
|
||||
- consumer1
|
||||
# rule 2: match by the domain.
|
||||
- _match_domain_:
|
||||
- "*.example.com"
|
||||
- test.com
|
||||
allow:
|
||||
- consumer2
|
||||
```
|
||||
In this sample, `route-a` and `route-b` specified in `_match_route_` are the route names filled in when creating gateway routes. When these two routes are matched, the caller with `name` as `consumer1` is allowed to access, and other callers are not allowed to access.
|
||||
|
||||
The `*.example.com` and `test.com` specified in `_match_domain_` are used to match the domain name of the request. When the domain name is matched, the caller with `name` as `consumer2` is allowed to access, and other callers are not allowed to access.
|
||||
|
||||
|
||||
#### According to this configuration, the following requests are allowed:
|
||||
|
||||
**Requests with specified username and password**
|
||||
|
||||
```bash
|
||||
# Assuming the following request will match with route-a
|
||||
# Use -u option of curl to specify the credentials
|
||||
curl -u admin:123456 http://xxx.hello.com/test
|
||||
# Or specify the Authorization request header directly with the credentials in base64 encoding
|
||||
curl -H 'Authorization: Basic YWRtaW46MTIzNDU2' http://xxx.hello.com/test
|
||||
```
|
||||
|
||||
A `X-Mse-Consumer` field will be added to the headers of the request, and its value in this example is `consumer1`, used to identify the name of the caller when passed authentication and authorization.
|
||||
|
||||
#### The following requests will be denied:
|
||||
|
||||
**Requests without providing username and password, returning 401**
|
||||
```bash
|
||||
curl http://xxx.hello.com/test
|
||||
```
|
||||
**Requests with incorrect username or password, returning 401**
|
||||
```bash
|
||||
curl -u admin:abc http://xxx.hello.com/test
|
||||
```
|
||||
**Requests matched with a caller who has no access permission, returning 403**
|
||||
```bash
|
||||
# consumer2 is not in the allow list of route-a
|
||||
curl -u guest:abc http://xxx.hello.com/test
|
||||
```
|
||||
|
||||
### Enable basic auth for gateway instance
|
||||
|
||||
The following configuration does not specify the `_rules_` field, so Basic Auth authentication will be effective for the whole gateway instance.
|
||||
|
||||
```yaml
|
||||
consumers:
|
||||
- credential: 'admin:123456'
|
||||
name: consumer1
|
||||
- credential: 'guest:abc'
|
||||
name: consumer2
|
||||
```
|
||||
|
||||
## Error Codes
|
||||
|
||||
| HTTP Status Code | Error Info | Reason |
|
||||
| ----------- |--------------------------------------------------------------------------------| ---------------------- |
|
||||
| 401 | Request denied by Basic Auth check. No Basic Authentication information found. | Credentials not provided in the request |
|
||||
| 401 | Request denied by Basic Auth check. Invalid username and/or password. | Invalid username and/or password |
|
||||
| 403 | Request denied by Basic Auth check. Unauthorized consumer. | Unauthorized consumer |
|
||||
1
plugins/wasm-go/extensions/basic-auth/VERSION
Normal file
1
plugins/wasm-go/extensions/basic-auth/VERSION
Normal file
@@ -0,0 +1 @@
|
||||
1.0.0
|
||||
22
plugins/wasm-go/extensions/basic-auth/go.mod
Normal file
22
plugins/wasm-go/extensions/basic-auth/go.mod
Normal file
@@ -0,0 +1,22 @@
|
||||
module github.com/alibaba/higress/plugins/wasm-go/extensions/basic-auth
|
||||
|
||||
go 1.19
|
||||
|
||||
|
||||
replace github.com/alibaba/higress/plugins/wasm-go => ../..
|
||||
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v0.0.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0
|
||||
github.com/tidwall/gjson v1.14.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/wasilibs/nottinygc v0.3.0 // indirect
|
||||
)
|
||||
9
plugins/wasm-go/extensions/basic-auth/go.sum
Normal file
9
plugins/wasm-go/extensions/basic-auth/go.sum
Normal file
@@ -0,0 +1,9 @@
|
||||
github.com/WeixinX/higress/plugins/wasm-go v0.0.0-20230911073755-f281286d0cdb/go.mod h1:shD9qvrDS6xklAVjKYho8kHIVdW4A1vhNEOAL2miEEE=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0/go.mod h1:qkW5MBz2jch2u8bS59wws65WC+Gtx3x0aPUX5JL7CXI=
|
||||
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/wasilibs/nottinygc v0.3.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK8jetlB3HIo=
|
||||
330
plugins/wasm-go/extensions/basic-auth/main.go
Normal file
330
plugins/wasm-go/extensions/basic-auth/main.go
Normal file
@@ -0,0 +1,330 @@
|
||||
// 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.
|
||||
|
||||
// The 'Basic' HTTP Authentication Scheme: https://datatracker.ietf.org/doc/html/rfc7617
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func main() {
|
||||
wrapper.SetCtx(
|
||||
"basic-auth",
|
||||
wrapper.ParseOverrideConfigBy(parseGlobalConfig, parseOverrideRuleConfig),
|
||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||
)
|
||||
}
|
||||
|
||||
// @Name basic-auth
|
||||
// @Category auth
|
||||
// @Phase AUTHN
|
||||
// @Priority 320
|
||||
// @Title zh-CN Basic Auth
|
||||
// @Description zh-CN 本插件实现了基于 HTTP Basic Auth 标准进行认证鉴权的功能。
|
||||
// @Description en-US This plugin implements an authentication function based on HTTP Basic Auth standard.
|
||||
// @IconUrl https://img.alicdn.com/imgextra/i4/O1CN01BPFGlT1pGZ2VDLgaH_!!6000000005333-2-tps-42-42.png
|
||||
// @Version 1.0.0
|
||||
//
|
||||
// @Contact.name Higress Team
|
||||
// @Contact.url http://higress.io/
|
||||
// @Contact.email admin@higress.io
|
||||
//
|
||||
// @Example
|
||||
// global_auth: false
|
||||
// consumers:
|
||||
// - name: consumer1
|
||||
// credential: admin:123456
|
||||
// - name: consumer2
|
||||
// credential: guest:abc
|
||||
//
|
||||
// @End
|
||||
type BasicAuthConfig struct {
|
||||
// @Title 是否开启全局认证
|
||||
// @Title en-US Enable Global Auth
|
||||
// @Description 若不开启全局认证,则全局配置只提供凭证信息。只有在域名或路由上进行了配置才会启用认证。
|
||||
// @Description en-US If set to false, only consumer info will be accepted from the global config. Auth feature shall only be enabled if the corresponding domain or route is configured.
|
||||
// @Scope GLOBAL
|
||||
globalAuth *bool `yaml:"global_auth"`
|
||||
|
||||
// @Title 调用方列表
|
||||
// @Title en-US Consumer List
|
||||
// @Description 服务调用方列表,用于对请求进行认证。
|
||||
// @Description en-US List of service consumers which will be used in request authentication.
|
||||
// @Scope GLOBAL
|
||||
consumers []Consumer `yaml:"consumers"`
|
||||
|
||||
// @Title 授权访问的调用方列表
|
||||
// @Title en-US Allowed Consumers
|
||||
// @Description 对于匹配上述条件的请求,允许访问的调用方列表。
|
||||
// @Description en-US Consumers to be allowed for matched requests.
|
||||
allow []string `yaml:"allow"`
|
||||
|
||||
credential2Name map[string]string `yaml:"-"`
|
||||
username2Passwd map[string]string `yaml:"-"`
|
||||
}
|
||||
|
||||
type Consumer struct {
|
||||
// @Title 名称
|
||||
// @Title en-US Name
|
||||
// @Description 该调用方的名称。
|
||||
// @Description en-US The name of the consumer.
|
||||
name string `yaml:"name"`
|
||||
|
||||
// @Title 访问凭证
|
||||
// @Title en-US Credential
|
||||
// @Description 该调用方的访问凭证。
|
||||
// @Description en-US The credential of the consumer.
|
||||
// @Scope GLOBAL
|
||||
credential string `yaml:"credential"`
|
||||
}
|
||||
|
||||
var (
|
||||
ruleSet bool // 插件是否至少在一个 domain 或 route 上生效
|
||||
protectionSpace = "MSE Gateway" // 认证失败时,返回响应头 WWW-Authenticate: Basic realm=MSE Gateway
|
||||
)
|
||||
|
||||
func parseGlobalConfig(json gjson.Result, global *BasicAuthConfig, log wrapper.Log) error {
|
||||
// log.Debug("global config")
|
||||
ruleSet = false
|
||||
global.credential2Name = make(map[string]string)
|
||||
global.username2Passwd = make(map[string]string)
|
||||
|
||||
consumers := json.Get("consumers")
|
||||
if !consumers.Exists() {
|
||||
return errors.New("consumers is required")
|
||||
}
|
||||
if len(consumers.Array()) == 0 {
|
||||
return errors.New("consumers cannot be empty")
|
||||
}
|
||||
|
||||
for _, item := range consumers.Array() {
|
||||
name := item.Get("name")
|
||||
if !name.Exists() || name.String() == "" {
|
||||
return errors.New("consumer name is required")
|
||||
}
|
||||
credential := item.Get("credential")
|
||||
if !credential.Exists() || credential.String() == "" {
|
||||
return errors.New("consumer credential is required")
|
||||
}
|
||||
if _, ok := global.credential2Name[credential.String()]; ok {
|
||||
return errors.Errorf("duplicate consumer credential: %s", credential.String())
|
||||
}
|
||||
userAndPasswd := strings.Split(credential.String(), ":")
|
||||
if len(userAndPasswd) != 2 {
|
||||
return errors.Errorf("invalid credential format: %s", credential.String())
|
||||
}
|
||||
|
||||
consumer := Consumer{
|
||||
name: name.String(),
|
||||
credential: credential.String(),
|
||||
}
|
||||
global.consumers = append(global.consumers, consumer)
|
||||
global.credential2Name[consumer.credential] = consumer.name
|
||||
global.username2Passwd[userAndPasswd[0]] = userAndPasswd[1]
|
||||
}
|
||||
|
||||
globalAuth := json.Get("global_auth")
|
||||
if globalAuth.Exists() {
|
||||
ga := globalAuth.Bool()
|
||||
global.globalAuth = &ga
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseOverrideRuleConfig(json gjson.Result, global BasicAuthConfig, config *BasicAuthConfig, log wrapper.Log) error {
|
||||
log.Debug("domain/route config")
|
||||
// override config via global
|
||||
*config = global
|
||||
|
||||
allow := json.Get("allow")
|
||||
if !allow.Exists() {
|
||||
return errors.New("allow is required")
|
||||
}
|
||||
if len(allow.Array()) == 0 {
|
||||
return errors.New("allow cannot be empty")
|
||||
}
|
||||
|
||||
for _, item := range allow.Array() {
|
||||
config.allow = append(config.allow, item.String())
|
||||
}
|
||||
ruleSet = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// basic-auth 插件认证逻辑:
|
||||
// - global_auth == true 开启全局生效:
|
||||
// - 若当前 domain/route 未配置 allow 列表,即未配置该插件:则在所有 consumers 中查找,如果找到则认证通过,否则认证失败 (1*)
|
||||
// - 若当前 domain/route 配置了该插件:则在 allow 列表中查找,如果找到则认证通过,否则认证失败
|
||||
//
|
||||
// - global_auth == false 非全局生效:(2*)
|
||||
// - 若当前 domain/route 未配置该插件:则直接放行
|
||||
// - 若当前 domain/route 配置了该插件:则在 allow 列表中查找,如果找到则认证通过,否则认证失败
|
||||
//
|
||||
// - global_auth 未设置:
|
||||
// - 若没有一个 domain/route 配置该插件:则遵循 (1*)
|
||||
// - 若有至少一个 domain/route 配置该插件:则遵循 (2*)
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config BasicAuthConfig, log wrapper.Log) types.Action {
|
||||
var (
|
||||
noAllow = len(config.allow) == 0 // 未配置 allow 列表,表示插件在该 domain/route 未生效
|
||||
globalAuthNoSet = config.globalAuth == nil
|
||||
globalAuthSetTrue = !globalAuthNoSet && *config.globalAuth
|
||||
globalAuthSetFalse = !globalAuthNoSet && !*config.globalAuth
|
||||
)
|
||||
// log.Debugf("global auth set: %t", !globalAuthNoSet)
|
||||
// log.Debugf("rule set: %t", ruleSet)
|
||||
// log.Debugf("config: %+v", config)
|
||||
|
||||
// 不需要认证而直接放行的情况:
|
||||
// - global_auth == false 且 当前 domain/route 未配置该插件
|
||||
// - global_auth 未设置 且 有至少一个 domain/route 配置该插件 且 当前 domain/route 未配置该插件
|
||||
if globalAuthSetFalse || (globalAuthNoSet && ruleSet) {
|
||||
if noAllow {
|
||||
log.Info("authorization is not required")
|
||||
return types.ActionContinue
|
||||
}
|
||||
}
|
||||
|
||||
// 以下为需要认证的情况:
|
||||
auth, err := proxywasm.GetHttpRequestHeader("Authorization")
|
||||
if err != nil {
|
||||
log.Warnf("failed to get authorization: %v", err)
|
||||
return deniedNoBasicAuthData()
|
||||
}
|
||||
if auth == "" {
|
||||
log.Warnf("authorization is empty")
|
||||
return deniedNoBasicAuthData()
|
||||
}
|
||||
if !strings.HasPrefix(auth, "Basic ") {
|
||||
log.Warnf("authorization has no prefix 'Basic '")
|
||||
return deniedNoBasicAuthData()
|
||||
}
|
||||
|
||||
encodedCredential := strings.TrimPrefix(auth, "Basic ")
|
||||
credentialByte, err := base64.StdEncoding.DecodeString(encodedCredential)
|
||||
if err != nil {
|
||||
log.Warnf("failed to decode authorization %q: %v", string(credentialByte), err)
|
||||
return deniedInvalidCredentials()
|
||||
}
|
||||
|
||||
credential := string(credentialByte)
|
||||
userAndPasswd := strings.Split(credential, ":")
|
||||
if len(userAndPasswd) != 2 {
|
||||
log.Warnf("invalid credential format: %s", credential)
|
||||
return deniedInvalidCredentials()
|
||||
}
|
||||
|
||||
user, passwd := userAndPasswd[0], userAndPasswd[1]
|
||||
if correctPasswd, ok := config.username2Passwd[user]; !ok {
|
||||
log.Warnf("credential username %q is not configured", user)
|
||||
return deniedInvalidCredentials()
|
||||
} else {
|
||||
if passwd != correctPasswd {
|
||||
log.Warnf("credential password is not correct for username %q", user)
|
||||
return deniedInvalidCredentials()
|
||||
}
|
||||
}
|
||||
|
||||
// 以下为 username 和 password 正确的情况:
|
||||
name, ok := config.credential2Name[credential]
|
||||
if !ok { // 理论上该分支永远不可达,因为 username 和 password 都是从 credential 中获取的
|
||||
log.Warnf("credential %q is not configured", credential)
|
||||
return deniedUnauthorizedConsumer()
|
||||
}
|
||||
|
||||
// 全局生效:
|
||||
// - global_auth == true 且 当前 domain/route 未配置该插件
|
||||
// - global_auth 未设置 且 没有任何一个 domain/route 配置该插件
|
||||
if (globalAuthSetTrue && noAllow) || (globalAuthNoSet && !ruleSet) {
|
||||
// log.Debug("authenticated case 1")
|
||||
log.Infof("consumer %q authenticated", name)
|
||||
return authenticated(name)
|
||||
}
|
||||
|
||||
// 全局生效,但当前 domain/route 配置了 allow 列表
|
||||
if globalAuthSetTrue && !noAllow {
|
||||
if !contains(config.allow, name) {
|
||||
log.Warnf("consumer %q is not allowed", name)
|
||||
return deniedUnauthorizedConsumer()
|
||||
}
|
||||
// log.Debug("authenticated case 2")
|
||||
log.Infof("consumer %q authenticated", name)
|
||||
return authenticated(name)
|
||||
}
|
||||
|
||||
// 非全局生效
|
||||
if globalAuthSetFalse || (globalAuthNoSet && ruleSet) {
|
||||
if !noAllow { // 配置了 allow 列表
|
||||
if !contains(config.allow, name) {
|
||||
log.Warnf("consumer %q is not allowed", name)
|
||||
return deniedUnauthorizedConsumer()
|
||||
}
|
||||
// log.Debug("authenticated case 3")
|
||||
log.Infof("consumer %q authenticated", name)
|
||||
return authenticated(name)
|
||||
}
|
||||
}
|
||||
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func deniedNoBasicAuthData() types.Action {
|
||||
_ = proxywasm.SendHttpResponse(401, WWWAuthenticateHeader(protectionSpace),
|
||||
[]byte("Request denied by Basic Auth check. No Basic Authentication information found."), -1)
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func deniedInvalidCredentials() types.Action {
|
||||
_ = proxywasm.SendHttpResponse(401, WWWAuthenticateHeader(protectionSpace),
|
||||
[]byte("Request denied by Basic Auth check. Invalid username and/or password."), -1)
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func deniedUnauthorizedConsumer() types.Action {
|
||||
_ = proxywasm.SendHttpResponse(403, WWWAuthenticateHeader(protectionSpace),
|
||||
[]byte("Request denied by Basic Auth check. Unauthorized consumer."), -1)
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func authenticated(name string) types.Action {
|
||||
_ = proxywasm.AddHttpRequestHeader("X-Mse-Consumer", name)
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func WWWAuthenticateHeader(realm string) [][2]string {
|
||||
return [][2]string{
|
||||
{"WWW-Authenticate", fmt.Sprintf("Basic realm=%s", realm)},
|
||||
}
|
||||
}
|
||||
|
||||
func contains(arr []string, item string) bool {
|
||||
for _, i := range arr {
|
||||
if i == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
15
plugins/wasm-go/extensions/jwt-auth/README.md
Normal file
15
plugins/wasm-go/extensions/jwt-auth/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# 功能说明
|
||||
`jwt-auth`插件基于wasm-go实现了Token解析认证功能,可以判断Token是否有效,如果Token有效则继续访问后端微服务,Token无效或不存在直接拒绝并返回401
|
||||
|
||||
# 配置字段
|
||||
| 名称 | 数据类型 | 填写要求 | 描述 |
|
||||
| ------------ | ------------ | ------------ | ------------ |
|
||||
| token_secret_key | string | 必填 | 配置Token解析使用的SecretKey|
|
||||
| token_headers | string | 必填 | 配置获取Token请求头名称|
|
||||
|
||||
# 配置示例
|
||||
```yaml
|
||||
token_secret_key: Dav7kfq3iA8S!JUj8&CUkdnQe72E@Cw6
|
||||
token_headers: token
|
||||
```
|
||||
此例`token_secret_key`中指定的是认证服务生成Token的SecretKey;`token_headers`是携带Token访问的请求头名称;
|
||||
1
plugins/wasm-go/extensions/jwt-auth/VERSION
Normal file
1
plugins/wasm-go/extensions/jwt-auth/VERSION
Normal file
@@ -0,0 +1 @@
|
||||
1.0.0
|
||||
18
plugins/wasm-go/extensions/jwt-auth/go.mod
Normal file
18
plugins/wasm-go/extensions/jwt-auth/go.mod
Normal file
@@ -0,0 +1,18 @@
|
||||
module jwt-auth
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v0.0.0-20230811015533-49269b43032f
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0
|
||||
github.com/tidwall/gjson v1.16.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/wasilibs/nottinygc v0.3.0 // indirect
|
||||
)
|
||||
22
plugins/wasm-go/extensions/jwt-auth/go.sum
Normal file
22
plugins/wasm-go/extensions/jwt-auth/go.sum
Normal file
@@ -0,0 +1,22 @@
|
||||
github.com/alibaba/higress/plugins/wasm-go v0.0.0-20230811015533-49269b43032f h1:H+2fEuroddobcGs2Vom+osc8CE3SBHLz+JbM036Lo9w=
|
||||
github.com/alibaba/higress/plugins/wasm-go v0.0.0-20230811015533-49269b43032f/go.mod h1:shD9qvrDS6xklAVjKYho8kHIVdW4A1vhNEOAL2miEEE=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0 h1:kS7BvMKN+FiptV4pfwiNX8e3q14evxAWkhYbxt8EI1M=
|
||||
github.com/tetratelabs/proxy-wasm-go-sdk v0.22.0/go.mod h1:qkW5MBz2jch2u8bS59wws65WC+Gtx3x0aPUX5JL7CXI=
|
||||
github.com/tidwall/gjson v1.16.0 h1:SyXa+dsSPpUlcwEDuKuEBJEz5vzTvOea+9rjyYodQFg=
|
||||
github.com/tidwall/gjson v1.16.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/wasilibs/nottinygc v0.3.0 h1:0L1jsJ1MsyN5tdinmFbLfuEA0TnHRcqaBM9pDTJVJmU=
|
||||
github.com/wasilibs/nottinygc v0.3.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK8jetlB3HIo=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
76
plugins/wasm-go/extensions/jwt-auth/main.go
Normal file
76
plugins/wasm-go/extensions/jwt-auth/main.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
|
||||
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// 自定义插件配置
|
||||
|
||||
func main() {
|
||||
wrapper.SetCtx(
|
||||
"jwt-auth", // 配置插件名称
|
||||
wrapper.ParseConfigBy(parseConfig),
|
||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||
)
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
TokenSecretKey string // 解析Token SecretKey
|
||||
TokenHeaders string // 定义获取Token请求头名称
|
||||
}
|
||||
|
||||
type Res struct {
|
||||
Code int `json:"code"` // 返回状态码
|
||||
Msg string `json:"msg"` // 返回信息
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, config *Config, log wrapper.Log) error {
|
||||
// 解析出配置,更新到config中
|
||||
config.TokenSecretKey = json.Get("token_secret_key").String()
|
||||
config.TokenHeaders = json.Get("token_headers").String()
|
||||
return nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config Config, log wrapper.Log) types.Action {
|
||||
var res Res
|
||||
if config.TokenHeaders == "" || config.TokenSecretKey == "" {
|
||||
res.Code = 401
|
||||
res.Msg = "参数不足"
|
||||
data, _ := json.Marshal(res)
|
||||
_ = proxywasm.SendHttpResponse(401, nil, data, -1)
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
token, err := proxywasm.GetHttpRequestHeader(config.TokenHeaders)
|
||||
if err != nil {
|
||||
res.Code = 401
|
||||
res.Msg = "认证失败"
|
||||
data, _ := json.Marshal(res)
|
||||
_ = proxywasm.SendHttpResponse(401, nil, data, -1)
|
||||
return types.ActionContinue
|
||||
}
|
||||
valid := ParseTokenValid(token, config.TokenSecretKey)
|
||||
if valid {
|
||||
_ = proxywasm.ResumeHttpRequest()
|
||||
return types.ActionPause
|
||||
} else {
|
||||
res.Code = 401
|
||||
res.Msg = "认证失败"
|
||||
data, _ := json.Marshal(res)
|
||||
_ = proxywasm.SendHttpResponse(401, nil, data, -1)
|
||||
return types.ActionContinue
|
||||
}
|
||||
}
|
||||
|
||||
func ParseTokenValid(tokenString, TokenSecretKey string) bool {
|
||||
token, _ := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
// 在这里提供用于验证签名的密钥
|
||||
return []byte(TokenSecretKey), nil
|
||||
})
|
||||
return token.Valid
|
||||
}
|
||||
@@ -67,7 +67,7 @@ static_resources:
|
||||
"useCRS": true,
|
||||
"secRules": [
|
||||
"SecDebugLogLevel 3",
|
||||
"SecRuleEngine On",
|
||||
"SecRuleEngine DetectionOnly",
|
||||
"SecRule REQUEST_URI \"@streq /admin\" \"id:101,phase:1,t:lowercase,deny\"",
|
||||
"SecRule REQUEST_BODY \"@rx maliciouspayload\" \"id:102,phase:2,t:lowercase,deny\"",
|
||||
"SecRule RESPONSE_HEADERS::status \"@rx 406\" \"id:103,phase:3,t:lowercase,deny\"",
|
||||
|
||||
47
plugins/wasm-go/extensions/waf/wasmplugin/logger.go
Normal file
47
plugins/wasm-go/extensions/waf/wasmplugin/logger.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package wasmplugin
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/corazawaf/coraza/v3/debuglog"
|
||||
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
|
||||
)
|
||||
|
||||
type logger struct {
|
||||
debuglog.Logger
|
||||
}
|
||||
|
||||
var _ debuglog.Logger = logger{}
|
||||
|
||||
var logPrinterFactory = func(io.Writer) debuglog.Printer {
|
||||
return func(lvl debuglog.Level, message, fields string) {
|
||||
switch lvl {
|
||||
case debuglog.LevelTrace:
|
||||
proxywasm.LogTracef("%s %s", message, fields)
|
||||
case debuglog.LevelDebug:
|
||||
proxywasm.LogDebugf("%s %s", message, fields)
|
||||
case debuglog.LevelInfo:
|
||||
proxywasm.LogInfof("%s %s", message, fields)
|
||||
case debuglog.LevelWarn:
|
||||
proxywasm.LogWarnf("%s %s", message, fields)
|
||||
case debuglog.LevelError:
|
||||
proxywasm.LogErrorf("%s %s", message, fields)
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultLogger() debuglog.Logger {
|
||||
return logger{
|
||||
debuglog.DefaultWithPrinterFactory(logPrinterFactory),
|
||||
}
|
||||
}
|
||||
|
||||
func (l logger) WithLevel(lvl debuglog.Level) debuglog.Logger {
|
||||
return logger{l.Logger.WithLevel(lvl)}
|
||||
}
|
||||
|
||||
func (l logger) WithOutput(_ io.Writer) debuglog.Logger {
|
||||
proxywasm.LogWarn("Ignoring SecDebugLog directive, debug logs are always routed to proxy logs")
|
||||
return l
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package wasmplugin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/corazawaf/coraza/v3/debuglog"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -50,8 +51,10 @@ func parseConfig(json gjson.Result, config *WafConfig, log wrapper.Log) error {
|
||||
}
|
||||
}
|
||||
|
||||
// log.Debugf("[rinfx log] %s", strings.Join(secRules, "\n"))
|
||||
conf := coraza.NewWAFConfig().WithRootFS(root)
|
||||
conf := coraza.NewWAFConfig().
|
||||
WithErrorCallback(logError).
|
||||
WithDebugLogger(debuglog.DefaultWithPrinterFactory(logPrinterFactory)).
|
||||
WithRootFS(root)
|
||||
// error: Failed to load Wasm module due to a missing import: wasi_snapshot_preview1.fd_filestat_get
|
||||
// because without fs.go
|
||||
waf, err := coraza.NewWAF(conf.WithDirectives(strings.Join(secRules, "\n")))
|
||||
@@ -86,7 +89,6 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config WafConfig, log wrapper
|
||||
// proxy-wasm.
|
||||
|
||||
if tx.IsRuleEngineOff() {
|
||||
// log.Infof("[rinfx log] OnHttpRequestHeaders, RuleEngine Off, url = %s", uri)
|
||||
return types.ActionContinue
|
||||
}
|
||||
// OnHttpRequestHeaders does not terminate if IP/Port retrieve goes wrong
|
||||
@@ -95,8 +97,6 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config WafConfig, log wrapper
|
||||
|
||||
tx.ProcessConnection(srcIP, srcPort, dstIP, dstPort)
|
||||
|
||||
// proxywasm.LogInfof("[rinfx log] OnHttpRequestHeaders, RuleEngine On, url = %s", uri)
|
||||
|
||||
method, err := proxywasm.GetHttpRequestHeader(":method")
|
||||
if err != nil {
|
||||
log.Error("Failed to get :method")
|
||||
@@ -140,10 +140,7 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config WafConfig, log wrapper
|
||||
}
|
||||
|
||||
func onHttpRequestBody(ctx wrapper.HttpContext, config WafConfig, body []byte, log wrapper.Log) types.Action {
|
||||
// log.Info("[rinfx log] OnHttpRequestBody")
|
||||
|
||||
if ctx.GetContext("interruptionHandled").(bool) {
|
||||
log.Error("OnHttpRequestBody, interruption already handled")
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
@@ -195,10 +192,7 @@ func onHttpRequestBody(ctx wrapper.HttpContext, config WafConfig, body []byte, l
|
||||
}
|
||||
|
||||
func onHttpResponseHeaders(ctx wrapper.HttpContext, config WafConfig, log wrapper.Log) types.Action {
|
||||
// log.Info("[rinfx log] OnHttpResponseHeaders")
|
||||
|
||||
if ctx.GetContext("interruptionHandled").(bool) {
|
||||
log.Error("OnHttpResponseHeaders, interruption already handled")
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
@@ -240,7 +234,6 @@ func onHttpResponseHeaders(ctx wrapper.HttpContext, config WafConfig, log wrappe
|
||||
|
||||
for _, h := range hs {
|
||||
tx.AddResponseHeader(h[0], h[1])
|
||||
// log.Infof("[rinfx debug] ResponseHeaders %s: %s", h[0], h[1])
|
||||
}
|
||||
|
||||
interruption := tx.ProcessResponseHeaders(code, ctx.GetContext("httpProtocol").(string))
|
||||
@@ -252,15 +245,13 @@ func onHttpResponseHeaders(ctx wrapper.HttpContext, config WafConfig, log wrappe
|
||||
}
|
||||
|
||||
func onHttpResponseBody(ctx wrapper.HttpContext, config WafConfig, body []byte, log wrapper.Log) types.Action {
|
||||
// log.Info("[rinfx log] OnHttpResponseBody")
|
||||
|
||||
if ctx.GetContext("interruptionHandled").(bool) {
|
||||
// At response body phase, proxy-wasm currently relies on emptying the response body as a way of
|
||||
// interruption the response. See https://github.com/corazawaf/coraza-proxy-wasm/issues/26.
|
||||
// If OnHttpResponseBody is called again and an interruption has already been raised, it means that
|
||||
// we have to keep going with the sanitization of the response, emptying it.
|
||||
// Sending the crafted HttpResponse with empty body, we don't expect to trigger OnHttpResponseBody
|
||||
log.Warn("Response body interruption already handled, keeping replacing the body")
|
||||
// log.Warn("Response body interruption already handled, keeping replacing the body")
|
||||
// Interruption happened, we don't want to send response body data
|
||||
return replaceResponseBodyWhenInterrupted(log, replaceResponseBody)
|
||||
}
|
||||
@@ -291,7 +282,6 @@ func onHttpResponseBody(ctx wrapper.HttpContext, config WafConfig, body []byte,
|
||||
}
|
||||
|
||||
interruption, _, err := tx.WriteResponseBody(body)
|
||||
// log.Infof("[rinfx debug] ResponseBody %s", string(body))
|
||||
if err != nil {
|
||||
log.Error("Failed to write response body")
|
||||
return types.ActionContinue
|
||||
@@ -316,8 +306,6 @@ func onHttpResponseBody(ctx wrapper.HttpContext, config WafConfig, body []byte,
|
||||
}
|
||||
|
||||
func onHttpStreamDone(ctx wrapper.HttpContext, config WafConfig, log wrapper.Log) {
|
||||
// log.Info("[rinfx log] OnHttpStreamDone")
|
||||
|
||||
tx := ctx.GetContext("tx").(ctypes.Transaction)
|
||||
|
||||
if !tx.IsRuleEngineOff() {
|
||||
@@ -335,5 +323,4 @@ func onHttpStreamDone(ctx wrapper.HttpContext, config WafConfig, log wrapper.Log
|
||||
tx.ProcessLogging()
|
||||
|
||||
_ = tx.Close()
|
||||
log.Info("Finished")
|
||||
}
|
||||
|
||||
@@ -83,8 +83,6 @@ func handleInterruption(ctx wrapper.HttpContext, phase string, interruption *cty
|
||||
panic("Interruption already handled")
|
||||
}
|
||||
|
||||
log.Infof("Transaction interrupted at %s", phase)
|
||||
|
||||
ctx.SetContext("interruptionHandled", true)
|
||||
if phase == "http_response_body" {
|
||||
return replaceResponseBodyWhenInterrupted(log, replaceResponseBody)
|
||||
@@ -117,3 +115,25 @@ func replaceResponseBodyWhenInterrupted(logger wrapper.Log, bodySize int) types.
|
||||
logger.Warn("Response body intervention occurred: body replaced")
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
func logError(error ctypes.MatchedRule) {
|
||||
msg := error.ErrorLog(0)
|
||||
switch error.Rule().Severity() {
|
||||
case ctypes.RuleSeverityEmergency:
|
||||
proxywasm.LogCritical(msg)
|
||||
case ctypes.RuleSeverityAlert:
|
||||
proxywasm.LogCritical(msg)
|
||||
case ctypes.RuleSeverityCritical:
|
||||
proxywasm.LogCritical(msg)
|
||||
case ctypes.RuleSeverityError:
|
||||
proxywasm.LogError(msg)
|
||||
case ctypes.RuleSeverityWarning:
|
||||
proxywasm.LogWarn(msg)
|
||||
case ctypes.RuleSeverityNotice:
|
||||
proxywasm.LogInfo(msg)
|
||||
case ctypes.RuleSeverityInfo:
|
||||
proxywasm.LogInfo(msg)
|
||||
case ctypes.RuleSeverityDebug:
|
||||
proxywasm.LogDebug(msg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,8 @@ func (m RuleMatcher[PluginConfig]) GetMatchConfig() (*PluginConfig, error) {
|
||||
}
|
||||
|
||||
func (m *RuleMatcher[PluginConfig]) ParseRuleConfig(config gjson.Result,
|
||||
parsePluginConfig func(gjson.Result, *PluginConfig) error) error {
|
||||
parsePluginConfig func(gjson.Result, *PluginConfig) error,
|
||||
parseOverrideConfig func(gjson.Result, PluginConfig, *PluginConfig) error) error {
|
||||
var rules []gjson.Result
|
||||
obj := config.Map()
|
||||
keyCount := len(obj)
|
||||
@@ -122,8 +123,15 @@ func (m *RuleMatcher[PluginConfig]) ParseRuleConfig(config gjson.Result,
|
||||
return fmt.Errorf("parse config failed, no valid rules; global config parse error:%v", globalConfigError)
|
||||
}
|
||||
for _, ruleJson := range rules {
|
||||
var rule RuleConfig[PluginConfig]
|
||||
err := parsePluginConfig(ruleJson, &rule.config)
|
||||
var (
|
||||
rule RuleConfig[PluginConfig]
|
||||
err error
|
||||
)
|
||||
if parseOverrideConfig != nil {
|
||||
err = parseOverrideConfig(ruleJson, m.globalConfig, &rule.config)
|
||||
} else {
|
||||
err = parsePluginConfig(ruleJson, &rule.config)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package matcher
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -221,7 +222,7 @@ func TestParseRuleConfig(t *testing.T) {
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
var actual RuleMatcher[customConfig]
|
||||
err := actual.ParseRuleConfig(gjson.Parse(c.config), parseConfig)
|
||||
err := actual.ParseRuleConfig(gjson.Parse(c.config), parseConfig, nil)
|
||||
if err != nil {
|
||||
if c.errMsg == "" {
|
||||
t.Errorf("parse failed: %v", err)
|
||||
@@ -236,3 +237,96 @@ func TestParseRuleConfig(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type completeConfig struct {
|
||||
// global config
|
||||
consumers []string
|
||||
// rule config
|
||||
allow []string
|
||||
}
|
||||
|
||||
func parseGlobalConfig(json gjson.Result, global *completeConfig) error {
|
||||
if json.Get("consumers").Exists() && json.Get("allow").Exists() {
|
||||
return errors.New("consumers and allow should not be configured at the same level")
|
||||
}
|
||||
|
||||
for _, item := range json.Get("consumers").Array() {
|
||||
global.consumers = append(global.consumers, item.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseOverrideRuleConfig(json gjson.Result, global completeConfig, config *completeConfig) error {
|
||||
if json.Get("consumers").Exists() && json.Get("allow").Exists() {
|
||||
return errors.New("consumers and allow should not be configured at the same level")
|
||||
}
|
||||
|
||||
// override config via global
|
||||
*config = global
|
||||
|
||||
for _, item := range json.Get("allow").Array() {
|
||||
config.allow = append(config.allow, item.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestParseOverrideConfig(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
config string
|
||||
errMsg string
|
||||
expected RuleMatcher[completeConfig]
|
||||
}{
|
||||
{
|
||||
name: "override rule config",
|
||||
config: `{"consumers":["c1","c2","c3"],"_rules_":[{"_match_route_":["r1","r2"],"allow":["c1","c3"]}]}`,
|
||||
expected: RuleMatcher[completeConfig]{
|
||||
ruleConfig: []RuleConfig[completeConfig]{
|
||||
{
|
||||
category: Route,
|
||||
routes: map[string]struct{}{
|
||||
"r1": {},
|
||||
"r2": {},
|
||||
},
|
||||
config: completeConfig{
|
||||
consumers: []string{"c1", "c2", "c3"},
|
||||
allow: []string{"c1", "c3"},
|
||||
},
|
||||
},
|
||||
},
|
||||
globalConfig: completeConfig{
|
||||
consumers: []string{"c1", "c2", "c3"},
|
||||
},
|
||||
hasGlobalConfig: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid config",
|
||||
config: `{"consumers":["c1","c2","c3"],"allow":["c1"]}`,
|
||||
errMsg: "parse config failed, no valid rules; global config parse error:consumers and allow should not be configured at the same level",
|
||||
},
|
||||
{
|
||||
name: "invalid config",
|
||||
config: `{"_rules_":[{"_match_route_":["r1","r2"],"consumers":["c1","c2"],"allow":["c1"]}]}`,
|
||||
errMsg: "consumers and allow should not be configured at the same level",
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
var actual RuleMatcher[completeConfig]
|
||||
err := actual.ParseRuleConfig(gjson.Parse(c.config), parseGlobalConfig, parseOverrideRuleConfig)
|
||||
if err != nil {
|
||||
if c.errMsg == "" {
|
||||
t.Errorf("parse failed: %v", err)
|
||||
}
|
||||
if err.Error() != c.errMsg {
|
||||
t.Errorf("expect err: %s, actual err: %s", c.errMsg, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
assert.Equal(t, c.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,3 +131,23 @@ func (c DnsCluster) ClusterName() string {
|
||||
func (c DnsCluster) HostName() string {
|
||||
return c.Domain
|
||||
}
|
||||
|
||||
type ConsulCluster struct {
|
||||
ServiceName string
|
||||
Datacenter string
|
||||
Port int64
|
||||
Host string
|
||||
}
|
||||
|
||||
func (c ConsulCluster) ClusterName() string {
|
||||
tail := "consul"
|
||||
return fmt.Sprintf("outbound|%d||%s.%s.%s",
|
||||
c.Port, c.ServiceName, c.Datacenter, tail)
|
||||
}
|
||||
|
||||
func (c ConsulCluster) HostName() string {
|
||||
if c.Host != "" {
|
||||
return c.Host
|
||||
}
|
||||
return c.ServiceName
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ type HttpContext interface {
|
||||
}
|
||||
|
||||
type ParseConfigFunc[PluginConfig any] func(json gjson.Result, config *PluginConfig, log Log) error
|
||||
type ParseRuleConfigFunc[PluginConfig any] func(json gjson.Result, global PluginConfig, config *PluginConfig, log Log) error
|
||||
type onHttpHeadersFunc[PluginConfig any] func(context HttpContext, config PluginConfig, log Log) types.Action
|
||||
type onHttpBodyFunc[PluginConfig any] func(context HttpContext, config PluginConfig, body []byte, log Log) types.Action
|
||||
type onHttpStreamDoneFunc[PluginConfig any] func(context HttpContext, config PluginConfig, log Log)
|
||||
@@ -54,6 +55,7 @@ type CommonVmCtx[PluginConfig any] struct {
|
||||
log Log
|
||||
hasCustomConfig bool
|
||||
parseConfig ParseConfigFunc[PluginConfig]
|
||||
parseRuleConfig ParseRuleConfigFunc[PluginConfig]
|
||||
onHttpRequestHeaders onHttpHeadersFunc[PluginConfig]
|
||||
onHttpRequestBody onHttpBodyFunc[PluginConfig]
|
||||
onHttpResponseHeaders onHttpHeadersFunc[PluginConfig]
|
||||
@@ -73,6 +75,13 @@ func ParseConfigBy[PluginConfig any](f ParseConfigFunc[PluginConfig]) SetPluginF
|
||||
}
|
||||
}
|
||||
|
||||
func ParseOverrideConfigBy[PluginConfig any](f ParseConfigFunc[PluginConfig], g ParseRuleConfigFunc[PluginConfig]) SetPluginFunc[PluginConfig] {
|
||||
return func(ctx *CommonVmCtx[PluginConfig]) {
|
||||
ctx.parseConfig = f
|
||||
ctx.parseRuleConfig = g
|
||||
}
|
||||
}
|
||||
|
||||
func ProcessRequestHeadersBy[PluginConfig any](f onHttpHeadersFunc[PluginConfig]) SetPluginFunc[PluginConfig] {
|
||||
return func(ctx *CommonVmCtx[PluginConfig]) {
|
||||
ctx.onHttpRequestHeaders = f
|
||||
@@ -161,9 +170,19 @@ func (ctx *CommonPluginCtx[PluginConfig]) OnPluginStart(int) types.OnPluginStart
|
||||
}
|
||||
jsonData = gjson.ParseBytes(data)
|
||||
}
|
||||
err = ctx.ParseRuleConfig(jsonData, func(js gjson.Result, cfg *PluginConfig) error {
|
||||
return ctx.vm.parseConfig(js, cfg, ctx.vm.log)
|
||||
})
|
||||
|
||||
var parseOverrideConfig func(gjson.Result, PluginConfig, *PluginConfig) error
|
||||
if ctx.vm.parseRuleConfig != nil {
|
||||
parseOverrideConfig = func(js gjson.Result, global PluginConfig, cfg *PluginConfig) error {
|
||||
return ctx.vm.parseRuleConfig(js, global, cfg, ctx.vm.log)
|
||||
}
|
||||
}
|
||||
err = ctx.ParseRuleConfig(jsonData,
|
||||
func(js gjson.Result, cfg *PluginConfig) error {
|
||||
return ctx.vm.parseConfig(js, cfg, ctx.vm.log)
|
||||
},
|
||||
parseOverrideConfig,
|
||||
)
|
||||
if err != nil {
|
||||
ctx.vm.log.Warnf("parse rule config failed: %v", err)
|
||||
return types.OnPluginStartStatusFailed
|
||||
|
||||
@@ -491,6 +491,7 @@ func (w *watcher) Stop() {
|
||||
}
|
||||
|
||||
w.isStop = true
|
||||
w.namingClient.CloseClient()
|
||||
close(w.stop)
|
||||
w.Ready(false)
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ func NewReconciler(serviceUpdate func(), client kube.Client, namespace string) *
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reconciler) Reconcile(mcpbridge *v1.McpBridge) {
|
||||
func (r *Reconciler) Reconcile(mcpbridge *v1.McpBridge) error {
|
||||
newRegistries := make(map[string]*apiv1.RegistryConfig)
|
||||
if mcpbridge != nil {
|
||||
for _, registry := range mcpbridge.Spec.Registries {
|
||||
@@ -121,12 +121,12 @@ func (r *Reconciler) Reconcile(mcpbridge *v1.McpBridge) {
|
||||
r.registries[k] = v
|
||||
}
|
||||
if errHappened {
|
||||
log.Error("ReconcileRegistries failed, Init Watchers failed")
|
||||
return
|
||||
return errors.New("ReconcileRegistries failed, Init Watchers failed")
|
||||
}
|
||||
wg.Wait()
|
||||
r.Cache.PurgeStaleService()
|
||||
log.Infof("Registries is reconciled")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Reconciler) generateWatcherFromRegistryConfig(registry *apiv1.RegistryConfig, wg *sync.WaitGroup) (Watcher, error) {
|
||||
|
||||
@@ -9,11 +9,11 @@ Higress e2e tests are mainly focusing on two parts for now:
|
||||
|
||||
### Architecture
|
||||
|
||||

|
||||

|
||||
|
||||
### Workflow
|
||||
|
||||

|
||||

|
||||
|
||||
Higress provides make target to run ingress api conformance tests and wasmplugin tests,
|
||||
|
||||
|
||||
119
test/e2e/conformance/base/consul.yaml
Normal file
119
test/e2e/conformance/base/consul.yaml
Normal file
@@ -0,0 +1,119 @@
|
||||
# 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.
|
||||
|
||||
# This file contains the base resources that most conformance tests will rely
|
||||
# on. This includes 3 namespaces along with Gateways, Services and Deployments
|
||||
# that can be used as backends for routing traffic. The most important
|
||||
# resources included are the Gateways (all in the higress-conformance-infra
|
||||
# namespace):
|
||||
# - same-namespace (only supports route in same ns)
|
||||
# - all-namespaces (supports routes in all ns)
|
||||
# - backend-namespaces (supports routes in ns with backend label)
|
||||
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: consul-standlone
|
||||
namespace: higress-conformance-app-backend
|
||||
labels:
|
||||
name: consul-standlone
|
||||
spec:
|
||||
containers:
|
||||
- name: consul
|
||||
image: docker.io/hashicorp/consul:1.16.0
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
ports:
|
||||
- containerPort: 8500
|
||||
name: http
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: consul-service
|
||||
namespace: higress-conformance-app-backend
|
||||
labels:
|
||||
name: consul-standlone
|
||||
spec:
|
||||
clusterIP: None
|
||||
ports:
|
||||
- name: http-query
|
||||
port: 8500
|
||||
protocol: TCP
|
||||
targetPort: 8500
|
||||
selector:
|
||||
name: consul-standlone
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: httpbin
|
||||
namespace: higress-conformance-app-backend
|
||||
spec:
|
||||
containers:
|
||||
- name: httpbin
|
||||
image: registry.cn-hangzhou.aliyuncs.com/2456868764/httpbin:1.0.2
|
||||
command:
|
||||
- /app/httpbin
|
||||
- --registry-type=consul
|
||||
- --consul-server-address=consul-service:8500
|
||||
- --server-port=8080
|
||||
- --service-tags=higress,httpbin
|
||||
env:
|
||||
- name: SERVICE_NAME
|
||||
value: httpbin
|
||||
- name: VERSION
|
||||
value: v1
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
- name: POD_NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- name: POD_IP
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: status.podIP
|
||||
- name: NODE_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.nodeName
|
||||
- name: SERVICE_ACCOUNT
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.serviceAccountName
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
protocol: TCP
|
||||
readinessProbe:
|
||||
failureThreshold: 5
|
||||
httpGet:
|
||||
path: /ping
|
||||
port: 8080
|
||||
scheme: HTTP
|
||||
periodSeconds: 20
|
||||
successThreshold: 1
|
||||
timeoutSeconds: 1
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
port: 8080
|
||||
scheme: HTTP
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 20
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user