mirror of
https://github.com/alibaba/higress.git
synced 2026-02-25 21:21:01 +08:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
792b9b0ee5 | ||
|
|
26ed9a6d93 | ||
|
|
ed36a4989f | ||
|
|
f23e26374f | ||
|
|
eb2934c084 | ||
|
|
2da1c62c69 | ||
|
|
fab734d39a | ||
|
|
2393af5c85 | ||
|
|
b142f51776 | ||
|
|
587267a733 | ||
|
|
a2078711f5 |
@@ -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,8 +174,8 @@ install: pre-install
|
||||
cd helm/higress; helm dependency build
|
||||
helm install higress helm/higress -n higress-system --create-namespace --set 'global.local=true'
|
||||
|
||||
ENVOY_LATEST_IMAGE_TAG ?= 1.1.1
|
||||
ISTIO_LATEST_IMAGE_TAG ?= 1.1.1
|
||||
ENVOY_LATEST_IMAGE_TAG ?= 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 'pilot.tag=$(ISTIO_LATEST_IMAGE_TAG)' --set 'gateway.tag=$(ENVOY_LATEST_IMAGE_TAG)' --set 'global.local=true'
|
||||
|
||||
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.2
|
||||
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.2
|
||||
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"]
|
||||
@@ -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.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
dependencies:
|
||||
- name: higress-core
|
||||
repository: file://../core
|
||||
version: 1.1.2
|
||||
version: 1.2.0
|
||||
- name: higress-console
|
||||
repository: https://higress.io/helm-charts/
|
||||
version: 1.1.2
|
||||
digest: sha256:8fc099c4ad77bcdc4b9dde2ef14f89b2159b6fdcc49a3dc7e1cccb01a7ed99b9
|
||||
generated: "2023-09-19T21:46:20.2567789+08:00"
|
||||
version: 1.2.0
|
||||
digest: sha256:d53c2da70cb3bcace50bce756acb50750a84c0888ec0d4c112939bf3c6e4daeb
|
||||
generated: "2023-09-22T16:52:34.940675+08:00"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
apiVersion: v2
|
||||
appVersion: 1.1.2
|
||||
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.2
|
||||
version: 1.2.0
|
||||
- name: higress-console
|
||||
repository: "https://higress.io/helm-charts/"
|
||||
version: 1.1.2
|
||||
version: 1.2.0
|
||||
type: application
|
||||
version: 1.1.2
|
||||
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)
|
||||
|
||||
21
pkg/cmd/hgctl/common.go
Normal file
21
pkg/cmd/hgctl/common.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package hgctl
|
||||
|
||||
const (
|
||||
yamlOutput = "yaml"
|
||||
jsonOutput = "json"
|
||||
flagsOutput = "flags"
|
||||
)
|
||||
@@ -33,8 +33,8 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
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"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,6 +491,7 @@ func (w *watcher) Stop() {
|
||||
}
|
||||
|
||||
w.isStop = true
|
||||
w.namingClient.CloseClient()
|
||||
close(w.stop)
|
||||
w.Ready(false)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user