diff --git a/go.mod b/go.mod index fe8c4f31e..341fcea4b 100644 --- a/go.mod +++ b/go.mod @@ -21,13 +21,16 @@ require ( github.com/dubbogo/go-zookeeper v1.0.4-0.20211212162352-f9d2183d89d5 github.com/dubbogo/gost v1.13.1 github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 + github.com/evanphx/json-patch/v5 v5.6.0 github.com/gogo/protobuf v1.3.2 github.com/golang/protobuf v1.5.2 github.com/google/go-cmp v0.5.9 + github.com/google/yamlfmt v0.10.0 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/hashicorp/consul/api v1.23.0 github.com/hashicorp/go-multierror v1.1.1 github.com/hudl/fargo v1.4.0 + github.com/kylelemons/godebug v1.1.0 github.com/nacos-group/nacos-sdk-go v1.0.8 github.com/nacos-group/nacos-sdk-go/v2 v2.1.2 github.com/pkg/errors v0.9.1 @@ -38,6 +41,7 @@ require ( google.golang.org/grpc v1.48.0 google.golang.org/protobuf v1.28.0 gopkg.in/yaml.v2 v2.4.0 + helm.sh/helm/v3 v3.7.1 istio.io/api v0.0.0-20211122181927-8da52c66ff23 istio.io/client-go v1.12.0-rc.1.0.20211118171212-b744b6f111e4 istio.io/gogo-genproto v0.0.0-20211115195057-0e34bdd2be67 @@ -63,18 +67,24 @@ 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 @@ -82,18 +92,21 @@ require ( github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect github.com/clbanning/mxj v1.8.4 // 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 @@ -107,10 +120,12 @@ require ( 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/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 github.com/golang/mock v1.6.0 // indirect + github.com/golang/snappy v0.0.3 // indirect github.com/google/btree v1.0.1 // indirect github.com/google/go-containerregistry v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect @@ -118,6 +133,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 +149,15 @@ 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.0 // indirect + github.com/kr/pretty v0.3.0 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lestrrat-go/backoff/v2 v2.0.7 // indirect github.com/lestrrat-go/blackmagic v1.0.0 // indirect github.com/lestrrat-go/httpcc v1.0.0 // indirect @@ -143,10 +166,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/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 +179,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 +199,18 @@ 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/rogpeppe/go-internal v1.6.1 // indirect + github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc // indirect github.com/russross/blackfriday v1.5.2 // indirect github.com/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 @@ -201,6 +234,7 @@ require ( google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20211020151524-b7c3a969101a // 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 @@ -208,10 +242,12 @@ require ( 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/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 diff --git a/go.sum b/go.sum index 5c7b85c3e..71792e436 100644 --- a/go.sum +++ b/go.sum @@ -102,6 +102,7 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= @@ -116,6 +117,7 @@ github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0 github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= +github.com/Masterminds/squirrel v1.5.0 h1:JukIZisrUXadA9pl3rMkjhiamxiB0cXiu+HGp/Y8cY8= github.com/Masterminds/squirrel v1.5.0/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Masterminds/vcs v1.13.1/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= @@ -150,6 +152,9 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/RageCage64/multilinediff v0.2.0 h1:yNSpSF5NXIrmo6bRIgO4Q0g7SXqFD4j/WEcBE+BdCFY= +github.com/RageCage64/multilinediff v0.2.0/go.mod h1:pKr+KLgP0gvRzA+yv0/IUaYQuBYN1ucWysvsL58aMP0= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= @@ -187,6 +192,7 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/avast/retry-go/v4 v4.3.4 h1:pHLkL7jvCvP317I8Ge+Km2Yhntv3SdkJm7uekkqbKhM= github.com/avast/retry-go/v4 v4.3.4/go.mod h1:rv+Nla6Vk3/ilU0H51VHddWHiwimzX66yZ0JT6T+UvE= @@ -211,16 +217,24 @@ github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edY github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bmatcuk/doublestar/v4 v4.6.0 h1:HTuxyug8GyFbRkrffIpzNCSK4luc0TY3wzXvzIZhEXc= +github.com/bmatcuk/doublestar/v4 v4.6.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/braydonk/yaml v0.7.0 h1:ySkqO7r0MGoCNhiRJqE0Xe9yhINMyvOAB3nFjgyJn2k= +github.com/braydonk/yaml v0.7.0/go.mod h1:hcm3h581tudlirk8XEUPDBAimBPbmnL0Y45hCRl47N4= github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng= github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ= github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= @@ -285,6 +299,7 @@ github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1 github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= +github.com/containerd/cgroups v1.0.1 h1:iJnMvco9XGvKUvNQkv88bE4uJXxRQH18efbKo9w5vHQ= github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= @@ -305,6 +320,7 @@ github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09Zvgq github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= github.com/containerd/containerd v1.5.2/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= +github.com/containerd/containerd v1.5.7 h1:rQyoYtj4KddB3bxG6SAqd4+08gePNyJjRqvOIfV3rkM= github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c= github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= @@ -408,6 +424,7 @@ github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8l github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/distribution/distribution/v3 v3.0.0-20210804104954-38ab4c606ee3/go.mod h1:gt38b7cvVKazi5XkHvINNytZXgTEntyhtyM3HQz46Nk= +github.com/distribution/distribution/v3 v3.0.0-20210926092439-1563384b69df h1:zafDqOsnugdrReF9Pe0wybnfFtEIaegSyHNIvnwKPVk= github.com/distribution/distribution/v3 v3.0.0-20210926092439-1563384b69df/go.mod h1:ZDZib/BOniVWcXcsy0voU8gR00znhe5VJm47d3H2Y5g= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/docker/cli v20.10.7+incompatible h1:pv/3NqibQKphWZiAskMzdz8w0PRbtTaEB+f6NwdU7Is= @@ -416,14 +433,18 @@ github.com/docker/distribution v0.0.0-20191216044856-a8371794149d h1:jC8tT/S0OGx github.com/docker/distribution v0.0.0-20191216044856-a8371794149d/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ= github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4= github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= @@ -464,6 +485,7 @@ github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGE github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= +github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/florianl/go-nflog/v2 v2.0.1/go.mod h1:g+SOgM/SuePn9bvS/eo3Ild7J71nSb29OzbxR+7cln0= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= @@ -483,6 +505,7 @@ github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5 github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= github.com/fvbommel/sortorder v1.0.1 h1:dSnXLt4mJYH25uDDGa3biZNQsozaUWDSWeKJ0qqFfzE= github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= +github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7 h1:LofdAjjjqCSXMwLGgOgnE+rdPuvX9DxCqaHwKy7i/ko= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -565,6 +588,7 @@ github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+ github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -572,8 +596,11 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8Wd github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gobuffalo/flect v0.2.0/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= github.com/gobuffalo/flect v0.2.3/go.mod h1:vmkQwuZYhN5Pc4ljYQZzP+1sq+NEkK+lh20jmEmX3jc= +github.com/gobuffalo/logger v1.0.3 h1:YaXOTHNPCvkqqA7w05A4v0k2tCdpr+sgFlgINbQ6gqc= github.com/gobuffalo/logger v1.0.3/go.mod h1:SoeejUwldiS7ZsyCBphOGURmWdwUFXs0J7TCjEhjKxM= +github.com/gobuffalo/packd v1.0.0 h1:6ERZvJHfe24rfFmA9OaoKBdC7+c9sydrytMg8SdFGBM= github.com/gobuffalo/packd v1.0.0/go.mod h1:6VTc4htmJRFB7u1m/4LeMTWjFoYrUiBkU9Fdec9hrhI= +github.com/gobuffalo/packr/v2 v2.8.1 h1:tkQpju6i3EtMXJ9uoF5GT6kB+LMTimDWD8Xvbz6zDVA= github.com/gobuffalo/packr/v2 v2.8.1/go.mod h1:c/PLlOuTU+p3SybaJATW3H6lX/iK7xEz5OeMf+NnJpg= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= @@ -646,6 +673,7 @@ github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8l github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= +github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -700,6 +728,8 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/yamlfmt v0.10.0 h1:0eR+Z3ZhkJ4uYIpEU/BcxpnqtkNDq8eCxon/Sj0YeRc= +github.com/google/yamlfmt v0.10.0/go.mod h1:jW0ice5/S1EBCHhIV9rkGVfUjyCXD1cTlddkKwI8TKo= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -719,6 +749,7 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGa github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= @@ -729,6 +760,7 @@ github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= @@ -824,6 +856,7 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jmoiron/sqlx v1.3.1 h1:aLN7YINNZ7cYOPK3QC83dbM6KT0NMqVMw961TqrejlE= github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= github.com/johnlanni/gost v1.11.23-0.20220713132522-0967a24036c6 h1:i9IP6menkNYRAOJQ27+81deRmcyyirLZRXR5+BIilV0= github.com/johnlanni/gost v1.11.23-0.20220713132522-0967a24036c6/go.mod h1:PhJ8+qZJx+Txjx1KthNPuVkCvUca0jRLgKWj/noGgeI= @@ -859,6 +892,7 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= +github.com/karrick/godirwalk v1.15.8 h1:7+rWAZPn9zuRxaIqqT8Ohs2Q2Ac0msBqwRdxNCr2VVs= github.com/karrick/godirwalk v1.15.8/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= @@ -887,7 +921,9 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/lestrrat-go/backoff/v2 v2.0.7 h1:i2SeK33aOFJlUNJZzf2IpXRBvqBBnaGXfY5Xaop/GsE= github.com/lestrrat-go/backoff/v2 v2.0.7/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= @@ -912,6 +948,7 @@ github.com/lestrrat/go-file-rotatelogs v0.0.0-20180223000712-d3151e2a480f/go.mod github.com/lestrrat/go-strftime v0.0.0-20180220042222-ba3bf9c1d042 h1:Bvq8AziQ5jFF4BHGAEDSqwPW1NJS3XshxbRCxtjFAZc= github.com/lestrrat/go-strftime v0.0.0-20180220042222-ba3bf9c1d042/go.mod h1:TPpsiPUEh0zFL1Snz4crhMlBe60PYxRHr5oFF3rRYg0= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= @@ -933,8 +970,11 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= +github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= +github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs= @@ -967,10 +1007,12 @@ github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mN github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.11/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= @@ -1020,12 +1062,14 @@ github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/moby v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible h1:NT0cwArZg/wGdvY8pzej4tPr+9WGmDdkF8Suj+mkz2g= github.com/moby/moby v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/sys/mountinfo v0.4.1 h1:1O+1cHA1aujwEwwVMa2Xm2l+gIpUHyd3+D+d7LZh1kM= github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= @@ -1041,6 +1085,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= @@ -1150,6 +1195,7 @@ github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrap github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= @@ -1222,6 +1268,7 @@ github.com/prometheus/statsd_exporter v0.21.0 h1:hA05Q5RFeIjgwKIYEdFd59xu5Wwaznf github.com/prometheus/statsd_exporter v0.21.0/go.mod h1:rbT83sZq2V+p73lHhPZfMc3MLCHmSHelCh9hSGYNLTQ= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= @@ -1229,6 +1276,7 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc h1:BD7uZqkN8CpjJtN/tScAKiccBikU4dlqe/gNrkRaPY4= github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc/go.mod h1:HFLT6i9iR4QBOF5rdCyjddC9t59ArqWJV2xx+jwcCMo= github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= @@ -1394,9 +1442,13 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI= github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE= github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= +github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= @@ -2103,6 +2155,7 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/gcfg.v1 v1.2.3 h1:m8OOJ4ccYHnx2f4gQwpno8nAX5OGOh7RLaaz0pj3Ogs= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw= gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= @@ -2194,6 +2247,7 @@ k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM= k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q= k8s.io/apiserver v0.21.3/go.mod h1:eDPWlZG6/cCCMj/JBcEpDoK+I+6i3r9GsChYBHSbAzU= k8s.io/apiserver v0.22.1/go.mod h1:2mcM6dzSt+XndzVQJX21Gx0/Klo7Aen7i0Ai6tIa400= +k8s.io/apiserver v0.22.2 h1:TdIfZJc6YNhu2WxeAOWq1TvukHF0Sfx0+ln4XK9qnL4= k8s.io/apiserver v0.22.2/go.mod h1:vrpMmbyjWrgdyOvZTSpsusQq5iigKNWv9o9KlDAbBHI= k8s.io/cli-runtime v0.22.1/go.mod h1:YqwGrlXeEk15Yn3em2xzr435UGwbrCw5x+COQoTYfoo= k8s.io/cli-runtime v0.22.2 h1:fsd9rFk9FSaVq4SUq1fM27c8CFGsYZUJ/3BkgmjYWuY= @@ -2272,6 +2326,7 @@ k8s.io/utils v0.0.0-20210820185131-d34e5cb4466e/go.mod h1:jPW/WVKK9YHAvNhRxK0md/ k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed h1:jAne/RjBTyawwAy0utX5eqigAwz/lQhTmy+Hr/Cpue4= k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +oras.land/oras-go v0.4.0 h1:u6+7D+raZDYHwlz/uOwNANiRmyYDSSMW7A9E1xXycUQ= oras.land/oras-go v0.4.0/go.mod h1:VJcU+VE4rkclUbum5C0O7deEZbBYnsnpbGSACwTjOcg= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/letsencrypt v0.0.3/go.mod h1:buyQKZ6IXrRnB7TdkHP0RyEybLx18HHyOSoTyoOLqNY= diff --git a/pkg/cmd/hgctl/common.go b/pkg/cmd/hgctl/common.go new file mode 100644 index 000000000..e08813998 --- /dev/null +++ b/pkg/cmd/hgctl/common.go @@ -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" +) diff --git a/pkg/cmd/hgctl/helm/common.go b/pkg/cmd/hgctl/helm/common.go new file mode 100644 index 000000000..249d81f10 --- /dev/null +++ b/pkg/cmd/hgctl/helm/common.go @@ -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 +} diff --git a/pkg/cmd/hgctl/helm/name/name.go b/pkg/cmd/hgctl/helm/name/name.go new file mode 100644 index 000000000..72dc03dc2 --- /dev/null +++ b/pkg/cmd/hgctl/helm/name/name.go @@ -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" +) diff --git a/pkg/cmd/hgctl/helm/object/objects.go b/pkg/cmd/hgctl/helm/object/objects.go new file mode 100644 index 000000000..d22163869 --- /dev/null +++ b/pkg/cmd/hgctl/helm/object/objects.go @@ -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 +} diff --git a/pkg/cmd/hgctl/helm/object/objects_test.go b/pkg/cmd/hgctl/helm/object/objects_test.go new file mode 100644 index 000000000..05d05b127 --- /dev/null +++ b/pkg/cmd/hgctl/helm/object/objects_test.go @@ -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)) + } + }) + } +} diff --git a/pkg/cmd/hgctl/helm/profile.go b/pkg/cmd/hgctl/helm/profile.go new file mode 100644 index 000000000..0dc4ec2eb --- /dev/null +++ b/pkg/cmd/hgctl/helm/profile.go @@ -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 +} diff --git a/pkg/cmd/hgctl/helm/render.go b/pkg/cmd/hgctl/helm/render.go new file mode 100644 index 000000000..d76d583cb --- /dev/null +++ b/pkg/cmd/hgctl/helm/render.go @@ -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) +} diff --git a/pkg/cmd/hgctl/helm/tpath/tree.go b/pkg/cmd/hgctl/helm/tpath/tree.go new file mode 100644 index 000000000..63b982578 --- /dev/null +++ b/pkg/cmd/hgctl/helm/tpath/tree.go @@ -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 "" +} diff --git a/pkg/cmd/hgctl/helm/tpath/tree_test.go b/pkg/cmd/hgctl/helm/tpath/tree_test.go new file mode 100644 index 000000000..a1edef9d2 --- /dev/null +++ b/pkg/cmd/hgctl/helm/tpath/tree_test.go @@ -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) + } + }) + } +} diff --git a/pkg/cmd/hgctl/helm/tpath/util.go b/pkg/cmd/hgctl/helm/tpath/util.go new file mode 100644 index 000000000..d1af56d1c --- /dev/null +++ b/pkg/cmd/hgctl/helm/tpath/util.go @@ -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 +} diff --git a/pkg/cmd/hgctl/helm/tpath/util_test.go b/pkg/cmd/hgctl/helm/tpath/util_test.go new file mode 100644 index 000000000..647f645b1 --- /dev/null +++ b/pkg/cmd/hgctl/helm/tpath/util_test.go @@ -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) + } + }) + } +} diff --git a/pkg/cmd/hgctl/install.go b/pkg/cmd/hgctl/install.go new file mode 100644 index 000000000..58f492254 --- /dev/null +++ b/pkg/cmd/hgctl/install.go @@ -0,0 +1,194 @@ +// Copyright (c) 2022 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package 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 + // verbose generates verbose output. + verbose bool +} + +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)) + } + + _, profile, profileName, err := helm.GenerateConfig(iArgs.InFilenames, setFlags) + if err != nil { + return fmt.Errorf("generate config: %v", err) + } + fmt.Fprintf(writer, "start to install higress on profile:%s ......\n", profileName) + + fmt.Fprintf(writer, "start to validate 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 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 + } + } + + return profileNameLocalK8s +} + +func InstallManifests(profile *helm.Profile, writer io.Writer) error { + fmt.Fprintf(writer, "start to check kubernetes cluster enviroment ......\n") + cliClient, err := kubernetes.NewCLIClient(options.DefaultConfigFlags.ToRawKubeConfigLoader()) + if err != nil { + return fmt.Errorf("failed to build kubernetes client: %w", err) + } + fmt.Fprintf(writer, "start to init higress installer ......\n") + op, err := installer.NewInstaller(profile, cliClient, writer) + if err != nil { + return err + } + fmt.Fprintf(writer, "start to run higress installer ......\n") + if err := op.Run(); err != nil { + return err + } + fmt.Fprintf(writer, "start to render manifests ......\n") + manifestMap, err := op.RenderManifests() + + //for name, yaml := range manifestMap { + // fileName := "~/Downloads/higress/manifests/" + string(name) + ".yaml" + // os.WriteFile(fileName, []byte(yaml), 0640) + //} + + if err != nil { + return err + } + fmt.Fprintf(writer, "start to apply manifests ......\n") + if err := op.ApplyManifests(manifestMap); err != nil { + return err + } + fmt.Fprintf(writer, "install higress complete!\n") + return nil +} diff --git a/pkg/cmd/hgctl/installer/component.go b/pkg/cmd/hgctl/installer/component.go new file mode 100644 index 000000000..d3d6ed107 --- /dev/null +++ b/pkg/cmd/hgctl/installer/component.go @@ -0,0 +1,295 @@ +// 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" + "github.com/alibaba/higress/pkg/cmd/hgctl/util" + "sigs.k8s.io/yaml" +) + +type ComponentName string + +const ( + Higress ComponentName = "higress" + Istio ComponentName = "istio" +) + +var ComponentMap = map[string]ComponentName{ + "higress": Higress, + "istio": 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 +} + +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 + } +} + +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 { + fmt.Fprintf(h.writer, "start to get higress helm chart latest version ......") + } + latestVersion, err := helm.ParseLatestVersion(h.opts.RepoURL, h.opts.Version) + if err != nil { + return err + } + fmt.Fprintf(h.writer, "latest version is %s\n", latestVersion) + + // Reset helm chart version + h.opts.Version = latestVersion + h.renderer.SetVersion(latestVersion) + fmt.Fprintf(h.writer, "start to download 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 + } + fmt.Fprintf(h.writer, "start to render 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 +} + +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 { + fmt.Fprintf(i.writer, "start to download 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 + } + fmt.Fprintf(i.writer, "start to render 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 +} + +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 +} diff --git a/pkg/cmd/hgctl/installer/installer.go b/pkg/cmd/hgctl/installer/installer.go new file mode 100644 index 000000000..f2d7f32a7 --- /dev/null +++ b/pkg/cmd/hgctl/installer/installer.go @@ -0,0 +1,193 @@ +// 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 +} + +// 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, "start to apply object kind: %s, object name: %s on namespace: %s ......\n", obj.Kind, obj.Name, obj.Namespace) + } else { + fmt.Fprintf(o.writer, "start to apply object kind: %s, object name: %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, "start to delete object kind: %s, object name: %s on namespace: %s ......\n", obj.Kind, obj.Name, obj.Namespace) + } else { + fmt.Fprintf(o.writer, "start to delete object kind: %s, object name: %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) (*Installer, error) { + if profile == nil { + return nil, errors.New("Install profile is empty") + } + // initialize components + components := make(map[ComponentName]Component) + higressComponent, err := NewHigressComponent(profile, writer, + WithComponentNamespace(profile.Global.Namespace), + WithComponentChartPath(profile.InstallPackagePath), + WithComponentVersion(profile.Charts.Higress.Version), + WithComponentRepoURL(profile.Charts.Higress.Url), + WithComponentChartName(profile.Charts.Higress.Name), + ) + if err != nil { + return nil, fmt.Errorf("NewHigressComponent failed, err: %s", err) + } + components[Higress] = higressComponent + + if profile.IstioEnabled() { + istioCRDComponent, err := NewIstioCRDComponent(profile, writer, + WithComponentNamespace(profile.Global.IstioNamespace), + WithComponentChartPath(profile.InstallPackagePath), + WithComponentVersion(profile.Charts.Istio.Version), + WithComponentRepoURL(profile.Charts.Istio.Url), + WithComponentChartName(profile.Charts.Istio.Name), + ) + 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 +} diff --git a/pkg/cmd/hgctl/kubernetes/client.go b/pkg/cmd/hgctl/kubernetes/client.go index 2093190e6..2a7ab0e9d 100644 --- a/pkg/cmd/hgctl/kubernetes/client.go +++ b/pkg/cmd/hgctl/kubernetes/client.go @@ -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 +} diff --git a/pkg/cmd/hgctl/kubernetes/common.go b/pkg/cmd/hgctl/kubernetes/common.go new file mode 100644 index 000000000..bf600ccf6 --- /dev/null +++ b/pkg/cmd/hgctl/kubernetes/common.go @@ -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 +} diff --git a/pkg/cmd/hgctl/manifests/manifest.go b/pkg/cmd/hgctl/manifests/manifest.go new file mode 100644 index 000000000..2549515f8 --- /dev/null +++ b/pkg/cmd/hgctl/manifests/manifest.go @@ -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) +} diff --git a/pkg/cmd/hgctl/manifests/profiles/_all.yaml b/pkg/cmd/hgctl/manifests/profiles/_all.yaml new file mode 100644 index 000000000..92d2fe5de --- /dev/null +++ b/pkg/cmd/hgctl/manifests/profiles/_all.yaml @@ -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 diff --git a/pkg/cmd/hgctl/manifests/profiles/k8s.yaml b/pkg/cmd/hgctl/manifests/profiles/k8s.yaml new file mode 100644 index 000000000..c2372e8c5 --- /dev/null +++ b/pkg/cmd/hgctl/manifests/profiles/k8s.yaml @@ -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 diff --git a/pkg/cmd/hgctl/manifests/profiles/local-k8s.yaml b/pkg/cmd/hgctl/manifests/profiles/local-k8s.yaml new file mode 100644 index 000000000..5310b9878 --- /dev/null +++ b/pkg/cmd/hgctl/manifests/profiles/local-k8s.yaml @@ -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 diff --git a/pkg/cmd/hgctl/profile.go b/pkg/cmd/hgctl/profile.go new file mode 100644 index 000000000..e46f2a0c0 --- /dev/null +++ b/pkg/cmd/hgctl/profile.go @@ -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 +} diff --git a/pkg/cmd/hgctl/profileDump.go b/pkg/cmd/hgctl/profileDump.go new file mode 100644 index 000000000..2caec5bb0 --- /dev/null +++ b/pkg/cmd/hgctl/profileDump.go @@ -0,0 +1,71 @@ +// 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" + "github.com/alibaba/higress/pkg/cmd/hgctl/helm" + "github.com/spf13/cobra" + "os" +) + +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 []", + 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 +} diff --git a/pkg/cmd/hgctl/profileList.go b/pkg/cmd/hgctl/profileList.go new file mode 100644 index 000000000..920c19eb6 --- /dev/null +++ b/pkg/cmd/hgctl/profileList.go @@ -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 +} diff --git a/pkg/cmd/hgctl/root.go b/pkg/cmd/hgctl/root.go index 374ca58f5..bf9f4fab1 100644 --- a/pkg/cmd/hgctl/root.go +++ b/pkg/cmd/hgctl/root.go @@ -28,6 +28,10 @@ func GetRootCommand() *cobra.Command { rootCmd.AddCommand(newVersionCommand()) rootCmd.AddCommand(newConfigCommand()) + rootCmd.AddCommand(newInstallCmd()) + rootCmd.AddCommand(newUninstallCmd()) + rootCmd.AddCommand(newUpgradeCmd()) + rootCmd.AddCommand(newProfileCmd()) return rootCmd } diff --git a/pkg/cmd/hgctl/uninstall.go b/pkg/cmd/hgctl/uninstall.go new file mode 100644 index 000000000..7dd7cd44a --- /dev/null +++ b/pkg/cmd/hgctl/uninstall.go @@ -0,0 +1,141 @@ +// Copyright (c) 2022 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package 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 + // verbose generates verbose output. + verbose bool +} + +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 { + if !promptUninstall(writer) { + return nil + } + + fmt.Fprintf(writer, "start to uninstall higress\n") + setFlags := make([]string, 0) + profileName := helm.GetUninstallProfileName() + fmt.Fprintf(writer, "start to uninstall higress profile:%s\n", profileName) + _, profile, err := helm.GenProfile(profileName, "", setFlags) + if err != nil { + return err + } + 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 := "" + fmt.Fprintf(writer, "Are you sure to uninstall higress?\n") + for { + fmt.Fprintf(writer, "Please input yes or no to select, input your selection:") + fmt.Scanln(&answer) + if strings.TrimSpace(answer) == "yes" { + return true + } + if strings.TrimSpace(answer) == "no" { + return false + } + } + + return false +} + +func UnInstallManifests(profile *helm.Profile, writer io.Writer) error { + fmt.Fprintf(writer, "start to check kubernetes cluster enviroment ......\n") + cliClient, err := kubernetes.NewCLIClient(options.DefaultConfigFlags.ToRawKubeConfigLoader()) + if err != nil { + return fmt.Errorf("failed to build kubernetes client: %w", err) + } + fmt.Fprintf(writer, "start to init higress installer ......\n") + op, err := installer.NewInstaller(profile, cliClient, writer) + if err != nil { + return err + } + fmt.Fprintf(writer, "start to run higress installer ......\n") + if err := op.Run(); err != nil { + return err + } + fmt.Fprintf(writer, "start to render manifests ......\n") + manifestMap, err := op.RenderManifests() + if err != nil { + return err + } + fmt.Fprintf(writer, "start to delete manifests ......\n") + if err := op.DeleteManifests(manifestMap); err != nil { + return err + } + fmt.Fprintf(writer, "uninstall higress complete!\n") + return nil +} diff --git a/pkg/cmd/hgctl/upgrade.go b/pkg/cmd/hgctl/upgrade.go new file mode 100644 index 000000000..9c66ac436 --- /dev/null +++ b/pkg/cmd/hgctl/upgrade.go @@ -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 +} diff --git a/pkg/cmd/hgctl/util/filter.go b/pkg/cmd/hgctl/util/filter.go new file mode 100644 index 000000000..bf3149b68 --- /dev/null +++ b/pkg/cmd/hgctl/util/filter.go @@ -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) +} diff --git a/pkg/cmd/hgctl/util/filter_test.go b/pkg/cmd/hgctl/util/filter_test.go new file mode 100644 index 000000000..3c5f216ec --- /dev/null +++ b/pkg/cmd/hgctl/util/filter_test.go @@ -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) + } + } +} diff --git a/pkg/cmd/hgctl/util/path.go b/pkg/cmd/hgctl/util/path.go new file mode 100644 index 000000000..fe19ecf14 --- /dev/null +++ b/pkg/cmd/hgctl/util/path.go @@ -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:] +} diff --git a/pkg/cmd/hgctl/util/path_test.go b/pkg/cmd/hgctl/util/path_test.go new file mode 100644 index 000000000..2c4604558 --- /dev/null +++ b/pkg/cmd/hgctl/util/path_test.go @@ -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) +} diff --git a/pkg/cmd/hgctl/util/reflect.go b/pkg/cmd/hgctl/util/reflect.go new file mode 100644 index 000000000..a494a27c6 --- /dev/null +++ b/pkg/cmd/hgctl/util/reflect.go @@ -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 +} diff --git a/pkg/cmd/hgctl/util/util.go b/pkg/cmd/hgctl/util/util.go new file mode 100644 index 000000000..969ac23cb --- /dev/null +++ b/pkg/cmd/hgctl/util/util.go @@ -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 +} diff --git a/pkg/cmd/hgctl/util/yaml.go b/pkg/cmd/hgctl/util/yaml.go new file mode 100644 index 000000000..5e76a295f --- /dev/null +++ b/pkg/cmd/hgctl/util/yaml.go @@ -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 == "" +} diff --git a/pkg/cmd/hgctl/util/yaml_test.go b/pkg/cmd/hgctl/util/yaml_test.go new file mode 100644 index 000000000..cd7d489cd --- /dev/null +++ b/pkg/cmd/hgctl/util/yaml_test.go @@ -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) + } + }) + } +} diff --git a/pkg/cmd/hgctl/version.go b/pkg/cmd/hgctl/version.go index 75b78467d..6e6e88685 100644 --- a/pkg/cmd/hgctl/version.go +++ b/pkg/cmd/hgctl/version.go @@ -34,8 +34,6 @@ import ( ) const ( - yamlOutput = "yaml" - jsonOutput = "json" higressCoreContainerName = "higress-core" higressGatewayContainerName = "higress-gateway" )