mirror of
https://github.com/alibaba/higress.git
synced 2026-05-08 04:17:27 +08:00
upgrade to istio 1.19 (#1211)
Co-authored-by: CH3CHO <ch3cho@qq.com> Co-authored-by: rinfx <893383980@qq.com>
This commit is contained in:
91
hgctl/pkg/util/filter.go
Normal file
91
hgctl/pkg/util/filter.go
Normal file
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/google/yamlfmt/formatters/basic"
|
||||
)
|
||||
|
||||
var (
|
||||
formatterConfig = func() *basic.Config {
|
||||
cfg := basic.DefaultConfig()
|
||||
return cfg
|
||||
}()
|
||||
formatter = &basic.BasicFormatter{
|
||||
Config: formatterConfig,
|
||||
Features: basic.ConfigureFeaturesFromConfig(formatterConfig),
|
||||
}
|
||||
)
|
||||
|
||||
// FilterFunc is used to filter some contents of manifest.
|
||||
type FilterFunc func(string) string
|
||||
|
||||
func ApplyFilters(input string, filters ...FilterFunc) string {
|
||||
for _, filter := range filters {
|
||||
input = filter(input)
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
// LicenseFilter assumes that license is at the beginning.
|
||||
// So we just remove all the leading comments until the first non-comment line appears.
|
||||
func LicenseFilter(input string) string {
|
||||
var index int
|
||||
buf := bufio.NewReader(strings.NewReader(input))
|
||||
for {
|
||||
line, err := buf.ReadString('\n')
|
||||
if !strings.HasPrefix(line, "#") {
|
||||
return input[index:]
|
||||
}
|
||||
index += len(line)
|
||||
if err == io.EOF {
|
||||
return input[index:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SpaceFilter removes all leading and trailing space.
|
||||
func SpaceFilter(input string) string {
|
||||
return strings.TrimSpace(input)
|
||||
}
|
||||
|
||||
// SpaceLineFilter removes all space lines.
|
||||
func SpaceLineFilter(input string) string {
|
||||
var builder strings.Builder
|
||||
scanner := bufio.NewScanner(strings.NewReader(input))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
builder.WriteString(line)
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// FormatterFilter uses github.com/google/yamlfmt to format yaml file
|
||||
func FormatterFilter(input string) string {
|
||||
resBytes, err := formatter.Format([]byte(input))
|
||||
// todo: think about log
|
||||
if err != nil {
|
||||
return input
|
||||
}
|
||||
return string(resBytes)
|
||||
}
|
||||
98
hgctl/pkg/util/filter_test.go
Normal file
98
hgctl/pkg/util/filter_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLicenseFilter(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
input: `# license line
|
||||
content line`,
|
||||
want: `content line`,
|
||||
},
|
||||
{
|
||||
input: `# license line`,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
input: `# license line
|
||||
content line
|
||||
# comment line`,
|
||||
want: `content line
|
||||
# comment line`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
res := LicenseFilter(test.input)
|
||||
if res != test.want {
|
||||
t.Errorf("want %s\n but got %s", test.want, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpaceFilter(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
input: `
|
||||
content line
|
||||
`,
|
||||
want: "content line",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
res := SpaceFilter(test.input)
|
||||
if res != test.want {
|
||||
t.Errorf("want %s\n but got %s", test.want, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatterFilter(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
input: `key1: val1 `,
|
||||
want: `key1: val1
|
||||
`,
|
||||
},
|
||||
{
|
||||
input: `key1:
|
||||
key2: val2`,
|
||||
want: `key1:
|
||||
key2: val2
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
res := FormatterFilter(test.input)
|
||||
if res != test.want {
|
||||
t.Errorf("want \n%s\n but got \n%s\n", test.want, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
123
hgctl/pkg/util/http_fetcher.go
Normal file
123
hgctl/pkg/util/http_fetcher.go
Normal file
@@ -0,0 +1,123 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultInitialInterval = 500 * time.Millisecond
|
||||
defaultMaxInterval = 60 * time.Second
|
||||
)
|
||||
|
||||
type HTTPFetcher struct {
|
||||
client *http.Client
|
||||
initialBackoff time.Duration
|
||||
requestMaxRetry int
|
||||
bufferSize int64
|
||||
}
|
||||
|
||||
// NewHTTPFetcher create a new HTTP remote fetcher.
|
||||
func NewHTTPFetcher(requestTimeout time.Duration, requestMaxRetry int, bufferSize int64) *HTTPFetcher {
|
||||
if requestTimeout == 0 {
|
||||
requestTimeout = 5 * time.Second
|
||||
}
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
// nolint: gosec
|
||||
// This is only when a user explicitly sets a flag to enable insecure mode
|
||||
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
return &HTTPFetcher{
|
||||
client: &http.Client{
|
||||
Timeout: requestTimeout,
|
||||
},
|
||||
initialBackoff: defaultInitialInterval,
|
||||
requestMaxRetry: requestMaxRetry,
|
||||
bufferSize: bufferSize,
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch downloads with HTTP get.
|
||||
func (f *HTTPFetcher) Fetch(ctx context.Context, url string) ([]byte, error) {
|
||||
c := f.client
|
||||
delayInterval := f.initialBackoff
|
||||
attempts := 0
|
||||
var lastError error
|
||||
for attempts < f.requestMaxRetry {
|
||||
attempts++
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := c.Do(req)
|
||||
if err != nil {
|
||||
lastError = err
|
||||
if ctx.Err() != nil {
|
||||
// If there is context timeout, exit this loop.
|
||||
return nil, fmt.Errorf("download failed after %v attempts, last error: %v", attempts, lastError)
|
||||
}
|
||||
delayInterval = delayInterval + f.initialBackoff
|
||||
if delayInterval > defaultMaxInterval {
|
||||
break
|
||||
}
|
||||
time.Sleep(delayInterval)
|
||||
continue
|
||||
}
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, f.bufferSize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return body, err
|
||||
}
|
||||
|
||||
lastError = fmt.Errorf("download request failed: status code %v", resp.StatusCode)
|
||||
|
||||
if retryable(resp.StatusCode) {
|
||||
_, err := io.ReadAll(io.LimitReader(resp.Body, f.bufferSize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = resp.Body.Close()
|
||||
delayInterval = delayInterval + f.initialBackoff
|
||||
if delayInterval > defaultMaxInterval {
|
||||
break
|
||||
}
|
||||
time.Sleep(delayInterval)
|
||||
continue
|
||||
}
|
||||
|
||||
err = resp.Body.Close()
|
||||
break
|
||||
|
||||
}
|
||||
return nil, fmt.Errorf("download failed after %v attempts, last error: %v", attempts, lastError)
|
||||
}
|
||||
|
||||
func retryable(code int) bool {
|
||||
return code >= 500 &&
|
||||
!(code == http.StatusNotImplemented ||
|
||||
code == http.StatusHTTPVersionNotSupported ||
|
||||
code == http.StatusNetworkAuthenticationRequired)
|
||||
}
|
||||
209
hgctl/pkg/util/path.go
Normal file
209
hgctl/pkg/util/path.go
Normal file
@@ -0,0 +1,209 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// PathSeparator is the separator between path elements.
|
||||
PathSeparator = "."
|
||||
// KVSeparator is the separator between the key and value in a key/value path element,
|
||||
KVSeparator = string(kvSeparatorRune)
|
||||
kvSeparatorRune = ':'
|
||||
|
||||
// InsertIndex is the index that means "insert" when setting values
|
||||
InsertIndex = -1
|
||||
|
||||
// PathSeparatorRune is the separator between path elements, as a rune.
|
||||
pathSeparatorRune = '.'
|
||||
// EscapedPathSeparator is what to use when the path shouldn't separate
|
||||
EscapedPathSeparator = "\\" + PathSeparator
|
||||
)
|
||||
|
||||
// ValidKeyRegex is a regex for a valid path key element.
|
||||
var ValidKeyRegex = regexp.MustCompile("^[a-zA-Z0-9_-]*$")
|
||||
|
||||
// Path is a path in slice form.
|
||||
type Path []string
|
||||
|
||||
// PathFromString converts a string path of form a.b.c to a string slice representation.
|
||||
func PathFromString(path string) Path {
|
||||
path = filepath.Clean(path)
|
||||
path = strings.TrimPrefix(path, PathSeparator)
|
||||
path = strings.TrimSuffix(path, PathSeparator)
|
||||
pv := splitEscaped(path, pathSeparatorRune)
|
||||
var r []string
|
||||
for _, str := range pv {
|
||||
if str != "" {
|
||||
str = strings.ReplaceAll(str, EscapedPathSeparator, PathSeparator)
|
||||
// Is str of the form node[expr], convert to "node", "[expr]"?
|
||||
nBracket := strings.IndexRune(str, '[')
|
||||
if nBracket > 0 {
|
||||
r = append(r, str[:nBracket], str[nBracket:])
|
||||
} else {
|
||||
// str is "[expr]" or "node"
|
||||
r = append(r, str)
|
||||
}
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// String converts a string slice path representation of form ["a", "b", "c"] to a string representation like "a.b.c".
|
||||
func (p Path) String() string {
|
||||
return strings.Join(p, PathSeparator)
|
||||
}
|
||||
|
||||
func (p Path) Equals(p2 Path) bool {
|
||||
if len(p) != len(p2) {
|
||||
return false
|
||||
}
|
||||
for i, pp := range p {
|
||||
if pp != p2[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ToYAMLPath converts a path string to path such that the first letter of each path element is lower case.
|
||||
func ToYAMLPath(path string) Path {
|
||||
p := PathFromString(path)
|
||||
for i := range p {
|
||||
p[i] = firstCharToLowerCase(p[i])
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// ToYAMLPathString converts a path string such that the first letter of each path element is lower case.
|
||||
func ToYAMLPathString(path string) string {
|
||||
return ToYAMLPath(path).String()
|
||||
}
|
||||
|
||||
// IsValidPathElement reports whether pe is a valid path element.
|
||||
func IsValidPathElement(pe string) bool {
|
||||
return ValidKeyRegex.MatchString(pe)
|
||||
}
|
||||
|
||||
// IsKVPathElement report whether pe is a key/value path element.
|
||||
func IsKVPathElement(pe string) bool {
|
||||
pe, ok := RemoveBrackets(pe)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
kv := splitEscaped(pe, kvSeparatorRune)
|
||||
if len(kv) != 2 || len(kv[0]) == 0 || len(kv[1]) == 0 {
|
||||
return false
|
||||
}
|
||||
return IsValidPathElement(kv[0])
|
||||
}
|
||||
|
||||
// IsVPathElement report whether pe is a value path element.
|
||||
func IsVPathElement(pe string) bool {
|
||||
pe, ok := RemoveBrackets(pe)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return len(pe) > 1 && pe[0] == ':'
|
||||
}
|
||||
|
||||
// IsNPathElement report whether pe is an index path element.
|
||||
func IsNPathElement(pe string) bool {
|
||||
pe, ok := RemoveBrackets(pe)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
n, err := strconv.Atoi(pe)
|
||||
return err == nil && n >= InsertIndex
|
||||
}
|
||||
|
||||
// PathKV returns the key and value string parts of the entire key/value path element.
|
||||
// It returns an error if pe is not a key/value path element.
|
||||
func PathKV(pe string) (k, v string, err error) {
|
||||
if !IsKVPathElement(pe) {
|
||||
return "", "", fmt.Errorf("%s is not a valid key:value path element", pe)
|
||||
}
|
||||
pe, _ = RemoveBrackets(pe)
|
||||
kv := splitEscaped(pe, kvSeparatorRune)
|
||||
return kv[0], kv[1], nil
|
||||
}
|
||||
|
||||
// PathV returns the value string part of the entire value path element.
|
||||
// It returns an error if pe is not a value path element.
|
||||
func PathV(pe string) (string, error) {
|
||||
// For :val, return the value only
|
||||
if IsVPathElement(pe) {
|
||||
v, _ := RemoveBrackets(pe)
|
||||
return v[1:], nil
|
||||
}
|
||||
|
||||
// For key:val, return the whole thing
|
||||
v, _ := RemoveBrackets(pe)
|
||||
if len(v) > 0 {
|
||||
return v, nil
|
||||
}
|
||||
return "", fmt.Errorf("%s is not a valid value path element", pe)
|
||||
}
|
||||
|
||||
// PathN returns the index part of the entire value path element.
|
||||
// It returns an error if pe is not an index path element.
|
||||
func PathN(pe string) (int, error) {
|
||||
if !IsNPathElement(pe) {
|
||||
return -1, fmt.Errorf("%s is not a valid index path element", pe)
|
||||
}
|
||||
v, _ := RemoveBrackets(pe)
|
||||
return strconv.Atoi(v)
|
||||
}
|
||||
|
||||
// RemoveBrackets removes the [] around pe and returns the resulting string. It returns false if pe is not surrounded
|
||||
// by [].
|
||||
func RemoveBrackets(pe string) (string, bool) {
|
||||
if !strings.HasPrefix(pe, "[") || !strings.HasSuffix(pe, "]") {
|
||||
return "", false
|
||||
}
|
||||
return pe[1 : len(pe)-1], true
|
||||
}
|
||||
|
||||
// splitEscaped splits a string using the rune r as a separator. It does not split on r if it's prefixed by \.
|
||||
func splitEscaped(s string, r rune) []string {
|
||||
var prev rune
|
||||
if len(s) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
prevIdx := 0
|
||||
var out []string
|
||||
for i, c := range s {
|
||||
if c == r && (i == 0 || (i > 0 && prev != '\\')) {
|
||||
out = append(out, s[prevIdx:i])
|
||||
prevIdx = i + 1
|
||||
}
|
||||
prev = c
|
||||
}
|
||||
out = append(out, s[prevIdx:])
|
||||
return out
|
||||
}
|
||||
|
||||
func firstCharToLowerCase(s string) string {
|
||||
return strings.ToLower(s[0:1]) + s[1:]
|
||||
}
|
||||
383
hgctl/pkg/util/path_test.go
Normal file
383
hgctl/pkg/util/path_test.go
Normal file
@@ -0,0 +1,383 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSplitEscaped(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
desc: "empty",
|
||||
in: "",
|
||||
want: []string{},
|
||||
},
|
||||
{
|
||||
desc: "no match",
|
||||
in: "foo",
|
||||
want: []string{"foo"},
|
||||
},
|
||||
{
|
||||
desc: "first",
|
||||
in: ":foo",
|
||||
want: []string{"", "foo"},
|
||||
},
|
||||
{
|
||||
desc: "last",
|
||||
in: "foo:",
|
||||
want: []string{"foo", ""},
|
||||
},
|
||||
{
|
||||
desc: "multiple",
|
||||
in: "foo:bar:baz",
|
||||
want: []string{"foo", "bar", "baz"},
|
||||
},
|
||||
{
|
||||
desc: "multiple with escapes",
|
||||
in: `foo\:bar:baz\:qux`,
|
||||
want: []string{`foo\:bar`, `baz\:qux`},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got, want := splitEscaped(tt.in, kvSeparatorRune), tt.want; !stringSlicesEqual(got, want) {
|
||||
t.Errorf("%s: got:%v, want:%v", tt.desc, got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsNPathElement(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in string
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
desc: "empty",
|
||||
in: "",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
desc: "negative",
|
||||
in: "[-45]",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
desc: "negative-1",
|
||||
in: "[-1]",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "valid",
|
||||
in: "[0]",
|
||||
expect: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got := IsNPathElement(tt.in); got != tt.expect {
|
||||
t.Errorf("%s: expect %v got %v", tt.desc, tt.expect, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func stringSlicesEqual(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i, aa := range a {
|
||||
if aa != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestPathFromString(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in string
|
||||
expect Path
|
||||
}{
|
||||
{
|
||||
desc: "no-path",
|
||||
in: "",
|
||||
expect: Path{},
|
||||
},
|
||||
{
|
||||
desc: "valid-path",
|
||||
in: "a.b.c",
|
||||
expect: Path{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
desc: "surround-periods",
|
||||
in: ".a.",
|
||||
expect: Path{"a"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got := PathFromString(tt.in); !got.Equals(tt.expect) {
|
||||
t.Errorf("%s: expect %v got %v", tt.desc, tt.expect, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToYAMLPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in string
|
||||
expect Path
|
||||
}{
|
||||
{
|
||||
desc: "all-uppercase",
|
||||
in: "A.B.C.D",
|
||||
expect: Path{"a", "b", "c", "d"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got := ToYAMLPath(tt.in); !got.Equals(tt.expect) {
|
||||
t.Errorf("%s: expect %v got %v", tt.desc, tt.expect, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsKVPathElement(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in string
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
desc: "valid",
|
||||
in: "[1:2]",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid",
|
||||
in: "[:2]",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
desc: "invalid-2",
|
||||
in: "[1:]",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
desc: "empty",
|
||||
in: "",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
desc: "no-brackets",
|
||||
in: "1:2",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
desc: "one-bracket",
|
||||
in: "[1:2",
|
||||
expect: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got := IsKVPathElement(tt.in); got != tt.expect {
|
||||
t.Errorf("%s: expect %v got %v", tt.desc, tt.expect, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsVPathElement(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in string
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
desc: "valid",
|
||||
in: "[:1]",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "kv-path-elem",
|
||||
in: "[1:2]",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
desc: "invalid",
|
||||
in: "1:2",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
desc: "empty",
|
||||
in: "",
|
||||
expect: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got := IsVPathElement(tt.in); got != tt.expect {
|
||||
t.Errorf("%s: expect %v got %v", tt.desc, tt.expect, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathKV(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in string
|
||||
wantK string
|
||||
wantV string
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
desc: "valid",
|
||||
in: "[1:2]",
|
||||
wantK: "1",
|
||||
wantV: "2",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "invalid",
|
||||
in: "[1:",
|
||||
wantErr: errors.New(""),
|
||||
},
|
||||
{
|
||||
desc: "empty",
|
||||
in: "",
|
||||
wantErr: errors.New(""),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if k, v, err := PathKV(tt.in); k != tt.wantK || v != tt.wantV || errNilCheck(err, tt.wantErr) {
|
||||
t.Errorf("%s: expect %v %v %v got %v %v %v", tt.desc, tt.wantK, tt.wantV, tt.wantErr, k, v, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathV(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in string
|
||||
want string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid-kv",
|
||||
in: "[1:2]",
|
||||
want: "1:2",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "valid-v",
|
||||
in: "[:1]",
|
||||
want: "1",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "invalid",
|
||||
in: "083fj",
|
||||
want: "",
|
||||
err: errors.New(""),
|
||||
},
|
||||
{
|
||||
desc: "empty",
|
||||
in: "",
|
||||
want: "",
|
||||
err: errors.New(""),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got, err := PathV(tt.in); got != tt.want || errNilCheck(err, tt.err) {
|
||||
t.Errorf("%s: expect %v %v got %v %v", tt.desc, tt.want, tt.err, got, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveBrackets(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in string
|
||||
expect string
|
||||
expectStat bool
|
||||
}{
|
||||
{
|
||||
desc: "has-brackets",
|
||||
in: "[yo]",
|
||||
expect: "yo",
|
||||
expectStat: true,
|
||||
},
|
||||
{
|
||||
desc: "one-bracket",
|
||||
in: "[yo",
|
||||
expect: "",
|
||||
expectStat: false,
|
||||
},
|
||||
{
|
||||
desc: "other-bracket",
|
||||
in: "yo]",
|
||||
expect: "",
|
||||
expectStat: false,
|
||||
},
|
||||
{
|
||||
desc: "no-brackets",
|
||||
in: "yo",
|
||||
expect: "",
|
||||
expectStat: false,
|
||||
},
|
||||
{
|
||||
desc: "empty",
|
||||
in: "",
|
||||
expect: "",
|
||||
expectStat: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got, stat := RemoveBrackets(tt.in); got != tt.expect || stat != tt.expectStat {
|
||||
t.Errorf("%s: expect %v %v got %v %v", tt.desc, tt.expect, tt.expectStat, got, stat)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func errNilCheck(err1, err2 error) bool {
|
||||
return (err1 == nil && err2 != nil) || (err1 != nil && err2 == nil)
|
||||
}
|
||||
311
hgctl/pkg/util/reflect.go
Normal file
311
hgctl/pkg/util/reflect.go
Normal file
@@ -0,0 +1,311 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// kindOf returns the reflection Kind that represents the dynamic type of value.
|
||||
// If value is a nil interface value, kindOf returns reflect.Invalid.
|
||||
func kindOf(value any) reflect.Kind {
|
||||
if value == nil {
|
||||
return reflect.Invalid
|
||||
}
|
||||
return reflect.TypeOf(value).Kind()
|
||||
}
|
||||
|
||||
// IsString reports whether value is a string type.
|
||||
func IsString(value any) bool {
|
||||
return kindOf(value) == reflect.String
|
||||
}
|
||||
|
||||
// IsPtr reports whether value is a ptr type.
|
||||
func IsPtr(value any) bool {
|
||||
return kindOf(value) == reflect.Ptr
|
||||
}
|
||||
|
||||
// IsMap reports whether value is a map type.
|
||||
func IsMap(value any) bool {
|
||||
return kindOf(value) == reflect.Map
|
||||
}
|
||||
|
||||
// IsMapPtr reports whether v is a map ptr type.
|
||||
func IsMapPtr(v any) bool {
|
||||
t := reflect.TypeOf(v)
|
||||
return t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Map
|
||||
}
|
||||
|
||||
// IsSlice reports whether value is a slice type.
|
||||
func IsSlice(value any) bool {
|
||||
return kindOf(value) == reflect.Slice
|
||||
}
|
||||
|
||||
// IsStruct reports whether value is a struct type
|
||||
func IsStruct(value any) bool {
|
||||
return kindOf(value) == reflect.Struct
|
||||
}
|
||||
|
||||
// IsSlicePtr reports whether v is a slice ptr type.
|
||||
func IsSlicePtr(v any) bool {
|
||||
return kindOf(v) == reflect.Ptr && reflect.TypeOf(v).Elem().Kind() == reflect.Slice
|
||||
}
|
||||
|
||||
// IsSliceInterfacePtr reports whether v is a slice ptr type.
|
||||
func IsSliceInterfacePtr(v any) bool {
|
||||
// Must use ValueOf because Elem().Elem() type resolves dynamically.
|
||||
vv := reflect.ValueOf(v)
|
||||
return vv.Kind() == reflect.Ptr && vv.Elem().Kind() == reflect.Interface && vv.Elem().Elem().Kind() == reflect.Slice
|
||||
}
|
||||
|
||||
// IsTypeStructPtr reports whether v is a struct ptr type.
|
||||
func IsTypeStructPtr(t reflect.Type) bool {
|
||||
if t == reflect.TypeOf(nil) {
|
||||
return false
|
||||
}
|
||||
return t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct
|
||||
}
|
||||
|
||||
// IsTypeSlicePtr reports whether v is a slice ptr type.
|
||||
func IsTypeSlicePtr(t reflect.Type) bool {
|
||||
if t == reflect.TypeOf(nil) {
|
||||
return false
|
||||
}
|
||||
return t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Slice
|
||||
}
|
||||
|
||||
// IsTypeMap reports whether v is a map type.
|
||||
func IsTypeMap(t reflect.Type) bool {
|
||||
if t == reflect.TypeOf(nil) {
|
||||
return false
|
||||
}
|
||||
return t.Kind() == reflect.Map
|
||||
}
|
||||
|
||||
// IsTypeInterface reports whether v is an interface.
|
||||
func IsTypeInterface(t reflect.Type) bool {
|
||||
if t == reflect.TypeOf(nil) {
|
||||
return false
|
||||
}
|
||||
return t.Kind() == reflect.Interface
|
||||
}
|
||||
|
||||
// IsTypeSliceOfInterface reports whether v is a slice of interface.
|
||||
func IsTypeSliceOfInterface(t reflect.Type) bool {
|
||||
if t == reflect.TypeOf(nil) {
|
||||
return false
|
||||
}
|
||||
return t.Kind() == reflect.Slice && t.Elem().Kind() == reflect.Interface
|
||||
}
|
||||
|
||||
// IsNilOrInvalidValue reports whether v is nil or reflect.Zero.
|
||||
func IsNilOrInvalidValue(v reflect.Value) bool {
|
||||
return !v.IsValid() || (v.Kind() == reflect.Ptr && v.IsNil()) || IsValueNil(v.Interface())
|
||||
}
|
||||
|
||||
// IsValueNil returns true if either value is nil, or has dynamic type {ptr,
|
||||
// map, slice} with value nil.
|
||||
func IsValueNil(value any) bool {
|
||||
if value == nil {
|
||||
return true
|
||||
}
|
||||
switch kindOf(value) {
|
||||
case reflect.Slice, reflect.Ptr, reflect.Map:
|
||||
return reflect.ValueOf(value).IsNil()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsValueNilOrDefault returns true if either IsValueNil(value) or the default
|
||||
// value for the type.
|
||||
func IsValueNilOrDefault(value any) bool {
|
||||
if IsValueNil(value) {
|
||||
return true
|
||||
}
|
||||
if !IsValueScalar(reflect.ValueOf(value)) {
|
||||
// Default value is nil for non-scalar types.
|
||||
return false
|
||||
}
|
||||
return value == reflect.New(reflect.TypeOf(value)).Elem().Interface()
|
||||
}
|
||||
|
||||
// IsValuePtr reports whether v is a ptr type.
|
||||
func IsValuePtr(v reflect.Value) bool {
|
||||
return v.Kind() == reflect.Ptr
|
||||
}
|
||||
|
||||
// IsValueInterface reports whether v is an interface type.
|
||||
func IsValueInterface(v reflect.Value) bool {
|
||||
return v.Kind() == reflect.Interface
|
||||
}
|
||||
|
||||
// IsValueStruct reports whether v is a struct type.
|
||||
func IsValueStruct(v reflect.Value) bool {
|
||||
return v.Kind() == reflect.Struct
|
||||
}
|
||||
|
||||
// IsValueStructPtr reports whether v is a struct ptr type.
|
||||
func IsValueStructPtr(v reflect.Value) bool {
|
||||
return v.Kind() == reflect.Ptr && IsValueStruct(v.Elem())
|
||||
}
|
||||
|
||||
// IsValueMap reports whether v is a map type.
|
||||
func IsValueMap(v reflect.Value) bool {
|
||||
return v.Kind() == reflect.Map
|
||||
}
|
||||
|
||||
// IsValueSlice reports whether v is a slice type.
|
||||
func IsValueSlice(v reflect.Value) bool {
|
||||
return v.Kind() == reflect.Slice
|
||||
}
|
||||
|
||||
// IsValueScalar reports whether v is a scalar type.
|
||||
func IsValueScalar(v reflect.Value) bool {
|
||||
if IsNilOrInvalidValue(v) {
|
||||
return false
|
||||
}
|
||||
if IsValuePtr(v) {
|
||||
if v.IsNil() {
|
||||
return false
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
return !IsValueStruct(v) && !IsValueMap(v) && !IsValueSlice(v)
|
||||
}
|
||||
|
||||
// ValuesAreSameType returns true if v1 and v2 has the same reflect.Type,
|
||||
// otherwise it returns false.
|
||||
func ValuesAreSameType(v1 reflect.Value, v2 reflect.Value) bool {
|
||||
return v1.Type() == v2.Type()
|
||||
}
|
||||
|
||||
// IsEmptyString returns true if value is an empty string.
|
||||
func IsEmptyString(value any) bool {
|
||||
if value == nil {
|
||||
return true
|
||||
}
|
||||
switch kindOf(value) {
|
||||
case reflect.String:
|
||||
if _, ok := value.(string); ok {
|
||||
return value.(string) == ""
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DeleteFromSlicePtr deletes an entry at index from the parent, which must be a slice ptr.
|
||||
func DeleteFromSlicePtr(parentSlice any, index int) error {
|
||||
pv := reflect.ValueOf(parentSlice)
|
||||
|
||||
if !IsSliceInterfacePtr(parentSlice) {
|
||||
return fmt.Errorf("deleteFromSlicePtr parent type is %T, must be *[]interface{}", parentSlice)
|
||||
}
|
||||
|
||||
pvv := pv.Elem()
|
||||
if pvv.Kind() == reflect.Interface {
|
||||
pvv = pvv.Elem()
|
||||
}
|
||||
|
||||
pv.Elem().Set(reflect.AppendSlice(pvv.Slice(0, index), pvv.Slice(index+1, pvv.Len())))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateSlicePtr updates an entry at index in the parent, which must be a slice ptr, with the given value.
|
||||
func UpdateSlicePtr(parentSlice any, index int, value any) error {
|
||||
pv := reflect.ValueOf(parentSlice)
|
||||
v := reflect.ValueOf(value)
|
||||
|
||||
if !IsSliceInterfacePtr(parentSlice) {
|
||||
return fmt.Errorf("updateSlicePtr parent type is %T, must be *[]interface{}", parentSlice)
|
||||
}
|
||||
|
||||
pvv := pv.Elem()
|
||||
if pvv.Kind() == reflect.Interface {
|
||||
pv.Elem().Elem().Index(index).Set(v)
|
||||
return nil
|
||||
}
|
||||
pv.Elem().Index(index).Set(v)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertIntoMap inserts value with key into parent which must be a map, map ptr, or interface to map.
|
||||
func InsertIntoMap(parentMap any, key any, value any) error {
|
||||
v := reflect.ValueOf(parentMap)
|
||||
kv := reflect.ValueOf(key)
|
||||
vv := reflect.ValueOf(value)
|
||||
|
||||
if v.Type().Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
if v.Type().Kind() == reflect.Interface {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if v.Type().Kind() != reflect.Map {
|
||||
return fmt.Errorf("insertIntoMap parent type is %T, must be map", parentMap)
|
||||
}
|
||||
|
||||
v.SetMapIndex(kv, vv)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteFromMap deletes an entry with the given key parent, which must be a map.
|
||||
func DeleteFromMap(parentMap any, key any) error {
|
||||
pv := reflect.ValueOf(parentMap)
|
||||
|
||||
if !IsMap(parentMap) {
|
||||
return fmt.Errorf("deleteFromMap parent type is %T, must be map", parentMap)
|
||||
}
|
||||
pv.SetMapIndex(reflect.ValueOf(key), reflect.Value{})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToIntValue returns 0, false if val is not a number type, otherwise it returns the int value of val.
|
||||
func ToIntValue(val any) (int, bool) {
|
||||
if IsValueNil(val) {
|
||||
return 0, false
|
||||
}
|
||||
v := reflect.ValueOf(val)
|
||||
switch {
|
||||
case IsIntKind(v.Kind()):
|
||||
return int(v.Int()), true
|
||||
case IsUintKind(v.Kind()):
|
||||
return int(v.Uint()), true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// IsIntKind reports whether k is an integer kind of any size.
|
||||
func IsIntKind(k reflect.Kind) bool {
|
||||
switch k {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsUintKind reports whether k is an unsigned integer kind of any size.
|
||||
func IsUintKind(k reflect.Kind) bool {
|
||||
switch k {
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
95
hgctl/pkg/util/util.go
Normal file
95
hgctl/pkg/util/util.go
Normal file
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"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
|
||||
}
|
||||
|
||||
// WriteFileString write string content to file
|
||||
func WriteFileString(fileName string, content string, perm os.FileMode) error {
|
||||
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, perm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
writer := bufio.NewWriter(file)
|
||||
if _, err := writer.WriteString(content); err != nil {
|
||||
return err
|
||||
}
|
||||
writer.Flush()
|
||||
return nil
|
||||
}
|
||||
318
hgctl/pkg/util/yaml.go
Normal file
318
hgctl/pkg/util/yaml.go
Normal file
@@ -0,0 +1,318 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
jsonpatch "github.com/evanphx/json-patch/v5" // nolint: staticcheck
|
||||
"github.com/kylelemons/godebug/diff"
|
||||
yaml3 "k8s.io/apimachinery/pkg/util/yaml"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
//
|
||||
//func ToYAMLGeneric(root any) ([]byte, error) {
|
||||
// var vs []byte
|
||||
// if proto, ok := root.(proto.Message); ok {
|
||||
// v, err := protomarshal.ToYAML(proto)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// vs = []byte(v)
|
||||
// } else {
|
||||
// v, err := yaml.Marshal(root)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// vs = v
|
||||
// }
|
||||
// return vs, nil
|
||||
//}
|
||||
//
|
||||
//func MustToYAMLGeneric(root any) string {
|
||||
// var vs []byte
|
||||
// if proto, ok := root.(proto.Message); ok {
|
||||
// v, err := protomarshal.ToYAML(proto)
|
||||
// if err != nil {
|
||||
// return err.Error()
|
||||
// }
|
||||
// vs = []byte(v)
|
||||
// } else {
|
||||
// v, err := yaml.Marshal(root)
|
||||
// if err != nil {
|
||||
// return err.Error()
|
||||
// }
|
||||
// vs = v
|
||||
// }
|
||||
// return string(vs)
|
||||
//}
|
||||
|
||||
// ToYAML returns a YAML string representation of val, or the error string if an error occurs.
|
||||
func ToYAML(val any) string {
|
||||
y, err := yaml.Marshal(val)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return string(y)
|
||||
}
|
||||
|
||||
//
|
||||
//// ToYAMLWithJSONPB returns a YAML string representation of val (using jsonpb), or the error string if an error occurs.
|
||||
//func ToYAMLWithJSONPB(val proto.Message) string {
|
||||
// v := reflect.ValueOf(val)
|
||||
// if val == nil || (v.Kind() == reflect.Ptr && v.IsNil()) {
|
||||
// return "null"
|
||||
// }
|
||||
// js, err := protomarshal.ToJSONWithOptions(val, "", true)
|
||||
// if err != nil {
|
||||
// return err.Error()
|
||||
// }
|
||||
// yb, err := yaml.JSONToYAML([]byte(js))
|
||||
// if err != nil {
|
||||
// return err.Error()
|
||||
// }
|
||||
// return string(yb)
|
||||
//}
|
||||
//
|
||||
//// MarshalWithJSONPB returns a YAML string representation of val (using jsonpb).
|
||||
//func MarshalWithJSONPB(val proto.Message) (string, error) {
|
||||
// return protomarshal.ToYAML(val)
|
||||
//}
|
||||
//
|
||||
//// UnmarshalWithJSONPB unmarshals y into out using gogo jsonpb (required for many proto defined structs).
|
||||
//func UnmarshalWithJSONPB(y string, out proto.Message, allowUnknownField bool) error {
|
||||
// // Treat nothing as nothing. If we called jsonpb.Unmarshaler it would return the same.
|
||||
// if y == "" {
|
||||
// return nil
|
||||
// }
|
||||
// jb, err := yaml.YAMLToJSON([]byte(y))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// if allowUnknownField {
|
||||
// err = protomarshal.UnmarshalAllowUnknown(jb, out)
|
||||
// } else {
|
||||
// err = protomarshal.Unmarshal(jb, out)
|
||||
// }
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// return nil
|
||||
//}
|
||||
|
||||
// OverlayTrees performs a sequential JSON strategic of overlays over base.
|
||||
func OverlayTrees(base map[string]any, overlays ...map[string]any) (map[string]any, error) {
|
||||
needsOverlay := false
|
||||
for _, o := range overlays {
|
||||
if len(o) > 0 {
|
||||
needsOverlay = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !needsOverlay {
|
||||
// Avoid expensive overlay if possible
|
||||
return base, nil
|
||||
}
|
||||
bby, err := yaml.Marshal(base)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
by := string(bby)
|
||||
|
||||
for _, o := range overlays {
|
||||
oy, err := yaml.Marshal(o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
by, err = OverlayYAML(by, string(oy))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
out := make(map[string]any)
|
||||
err = yaml.Unmarshal([]byte(by), &out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// OverlayYAML patches the overlay tree over the base tree and returns the result. All trees are expressed as YAML
|
||||
// strings.
|
||||
func OverlayYAML(base, overlay string) (string, error) {
|
||||
if strings.TrimSpace(base) == "" {
|
||||
return overlay, nil
|
||||
}
|
||||
if strings.TrimSpace(overlay) == "" {
|
||||
return base, nil
|
||||
}
|
||||
bj, err := yaml.YAMLToJSON([]byte(base))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("yamlToJSON error in base: %s\n%s", err, bj)
|
||||
}
|
||||
oj, err := yaml.YAMLToJSON([]byte(overlay))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("yamlToJSON error in overlay: %s\n%s", err, oj)
|
||||
}
|
||||
if base == "" {
|
||||
bj = []byte("{}")
|
||||
}
|
||||
if overlay == "" {
|
||||
oj = []byte("{}")
|
||||
}
|
||||
|
||||
merged, err := jsonpatch.MergePatch(bj, oj)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("json merge error (%s) for base object: \n%s\n override object: \n%s", err, bj, oj)
|
||||
}
|
||||
my, err := yaml.JSONToYAML(merged)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("jsonToYAML error (%s) for merged object: \n%s", err, merged)
|
||||
}
|
||||
|
||||
return string(my), nil
|
||||
}
|
||||
|
||||
// yamlDiff compares single YAML file
|
||||
func yamlDiff(a, b string) string {
|
||||
ao, bo := make(map[string]any), make(map[string]any)
|
||||
if err := yaml.Unmarshal([]byte(a), &ao); err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
if err := yaml.Unmarshal([]byte(b), &bo); err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
ay, err := yaml.Marshal(ao)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
by, err := yaml.Marshal(bo)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
return diff.Diff(string(ay), string(by))
|
||||
}
|
||||
|
||||
// yamlStringsToList yaml string parse to string list
|
||||
func yamlStringsToList(str string) []string {
|
||||
reader := bufio.NewReader(strings.NewReader(str))
|
||||
decoder := yaml3.NewYAMLReader(reader)
|
||||
res := make([]string, 0)
|
||||
for {
|
||||
doc, err := decoder.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
chunk := bytes.TrimSpace(doc)
|
||||
res = append(res, string(chunk))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// multiYamlDiffOutput multi yaml diff output format
|
||||
func multiYamlDiffOutput(res, diff string) string {
|
||||
if res == "" {
|
||||
return diff
|
||||
}
|
||||
if diff == "" {
|
||||
return res
|
||||
}
|
||||
|
||||
return res + "\n" + diff
|
||||
}
|
||||
|
||||
func diffStringList(l1, l2 []string) string {
|
||||
var maxLen int
|
||||
var minLen int
|
||||
var l1Max bool
|
||||
res := ""
|
||||
if len(l1)-len(l2) > 0 {
|
||||
maxLen = len(l1)
|
||||
minLen = len(l2)
|
||||
l1Max = true
|
||||
} else {
|
||||
maxLen = len(l2)
|
||||
minLen = len(l1)
|
||||
l1Max = false
|
||||
}
|
||||
|
||||
for i := 0; i < maxLen; i++ {
|
||||
d := ""
|
||||
if i >= minLen {
|
||||
if l1Max {
|
||||
d = yamlDiff(l1[i], "")
|
||||
} else {
|
||||
d = yamlDiff("", l2[i])
|
||||
}
|
||||
} else {
|
||||
d = yamlDiff(l1[i], l2[i])
|
||||
}
|
||||
res = multiYamlDiffOutput(res, d)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// YAMLDiff compares multiple YAML files and single YAML file
|
||||
func YAMLDiff(a, b string) string {
|
||||
al := yamlStringsToList(a)
|
||||
bl := yamlStringsToList(b)
|
||||
res := diffStringList(al, bl)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// IsYAMLEqual reports whether the YAML in strings a and b are equal.
|
||||
func IsYAMLEqual(a, b string) bool {
|
||||
if strings.TrimSpace(a) == "" && strings.TrimSpace(b) == "" {
|
||||
return true
|
||||
}
|
||||
ajb, err := yaml.YAMLToJSON([]byte(a))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
bjb, err := yaml.YAMLToJSON([]byte(b))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return bytes.Equal(ajb, bjb)
|
||||
}
|
||||
|
||||
// IsYAMLEmpty reports whether the YAML string y is logically empty.
|
||||
func IsYAMLEmpty(y string) bool {
|
||||
var yc []string
|
||||
for _, l := range strings.Split(y, "\n") {
|
||||
yt := strings.TrimSpace(l)
|
||||
if !strings.HasPrefix(yt, "#") && !strings.HasPrefix(yt, "---") {
|
||||
yc = append(yc, l)
|
||||
}
|
||||
}
|
||||
res := strings.TrimSpace(strings.Join(yc, "\n"))
|
||||
return res == "{}" || res == ""
|
||||
}
|
||||
363
hgctl/pkg/util/yaml_test.go
Normal file
363
hgctl/pkg/util/yaml_test.go
Normal file
@@ -0,0 +1,363 @@
|
||||
// Copyright (c) 2022 Alibaba Group Holding Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestToYAML(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
inVals any
|
||||
expectedOut string
|
||||
}{
|
||||
{
|
||||
desc: "valid-yaml",
|
||||
inVals: map[string]any{
|
||||
"foo": "bar",
|
||||
"yo": map[string]any{
|
||||
"istio": "bar",
|
||||
},
|
||||
},
|
||||
expectedOut: `foo: bar
|
||||
yo:
|
||||
istio: bar
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "alphabetical",
|
||||
inVals: map[string]any{
|
||||
"foo": "yaml",
|
||||
"abc": "f",
|
||||
},
|
||||
expectedOut: `abc: f
|
||||
foo: yaml
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "expected-err-nil",
|
||||
inVals: nil,
|
||||
expectedOut: "null\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got := ToYAML(tt.inVals); got != tt.expectedOut {
|
||||
t.Errorf("%s: expected out %v got %s", tt.desc, tt.expectedOut, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverlayTrees(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
inBase map[string]any
|
||||
inOverlays map[string]any
|
||||
expectedOverlay map[string]any
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
desc: "overlay-valid",
|
||||
inBase: map[string]any{
|
||||
"foo": "bar",
|
||||
"baz": "naz",
|
||||
},
|
||||
inOverlays: map[string]any{
|
||||
"foo": "laz",
|
||||
},
|
||||
expectedOverlay: map[string]any{
|
||||
"baz": "naz",
|
||||
"foo": "laz",
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "overlay-key-does-not-exist",
|
||||
inBase: map[string]any{
|
||||
"foo": "bar",
|
||||
"baz": "naz",
|
||||
},
|
||||
inOverlays: map[string]any{
|
||||
"i-dont-exist": "i-really-dont-exist",
|
||||
},
|
||||
expectedOverlay: map[string]any{
|
||||
"baz": "naz",
|
||||
"foo": "bar",
|
||||
"i-dont-exist": "i-really-dont-exist",
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "remove-key-val",
|
||||
inBase: map[string]any{
|
||||
"foo": "bar",
|
||||
},
|
||||
inOverlays: map[string]any{
|
||||
"foo": nil,
|
||||
},
|
||||
expectedOverlay: map[string]any{},
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if gotOverlays, err := OverlayTrees(tt.inBase, tt.inOverlays); !reflect.DeepEqual(gotOverlays, tt.expectedOverlay) ||
|
||||
((err != nil && tt.expectedErr == nil) || (err == nil && tt.expectedErr != nil)) {
|
||||
t.Errorf("%s: expected overlay & err %v %v got %v %v", tt.desc, tt.expectedOverlay, tt.expectedErr,
|
||||
gotOverlays, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverlayYAML(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
base string
|
||||
overlay string
|
||||
expect string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "overlay-yaml",
|
||||
base: `foo: bar
|
||||
yo: lo
|
||||
`,
|
||||
overlay: `yo: go`,
|
||||
expect: `foo: bar
|
||||
yo: go
|
||||
`,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "combine-yaml",
|
||||
base: `foo: bar`,
|
||||
overlay: `baz: razmatazz`,
|
||||
expect: `baz: razmatazz
|
||||
foo: bar
|
||||
`,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "blank",
|
||||
base: `R#)*J#FN`,
|
||||
overlay: `FM#)M#F(*#M`,
|
||||
expect: "",
|
||||
err: errors.New("invalid json"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got, err := OverlayYAML(tt.base, tt.overlay); got != tt.expect || ((tt.err != nil && err == nil) || (tt.err == nil && err != nil)) {
|
||||
t.Errorf("%s: expected overlay&err %v %v got %v %v", tt.desc, tt.expect, tt.err, got, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestYAMLDiff(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
diff1 string
|
||||
diff2 string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
desc: "1-line-diff",
|
||||
diff1: `hola: yo
|
||||
foo: bar
|
||||
goo: tar
|
||||
`,
|
||||
diff2: `hola: yo
|
||||
foo: bar
|
||||
notgoo: nottar
|
||||
`,
|
||||
expect: ` foo: bar
|
||||
-goo: tar
|
||||
hola: yo
|
||||
+notgoo: nottar
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "no-diff",
|
||||
diff1: `foo: bar`,
|
||||
diff2: `foo: bar`,
|
||||
expect: ``,
|
||||
},
|
||||
{
|
||||
desc: "invalid-yaml",
|
||||
diff1: `Ij#**#f#`,
|
||||
diff2: `fm*##)n`,
|
||||
expect: "error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type map[string]interface {}",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got := YAMLDiff(tt.diff1, tt.diff2); got != tt.expect {
|
||||
t.Errorf("%s: expect %v got %v", tt.desc, tt.expect, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleYAMLDiff(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
diff1 string
|
||||
diff2 string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
desc: "1-line-diff",
|
||||
diff1: `hola: yo
|
||||
foo: bar
|
||||
goo: tar
|
||||
---
|
||||
hola: yo1
|
||||
foo: bar1
|
||||
goo: tar1
|
||||
`,
|
||||
diff2: `hola: yo
|
||||
foo: bar
|
||||
notgoo: nottar
|
||||
`,
|
||||
expect: ` foo: bar
|
||||
-goo: tar
|
||||
hola: yo
|
||||
+notgoo: nottar
|
||||
|
||||
-foo: bar1
|
||||
-goo: tar1
|
||||
-hola: yo1
|
||||
+{}
|
||||
`,
|
||||
},
|
||||
{
|
||||
desc: "no-diff",
|
||||
diff1: `foo: bar
|
||||
---
|
||||
foo: bar1
|
||||
`,
|
||||
diff2: `foo: bar
|
||||
---
|
||||
foo: bar1
|
||||
`,
|
||||
expect: ``,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got := YAMLDiff(tt.diff1, tt.diff2); got != tt.expect {
|
||||
t.Errorf("%s: expect %v got %v", tt.desc, tt.expect, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsYAMLEqual(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in1 string
|
||||
in2 string
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
desc: "yaml-equal",
|
||||
in1: `foo: bar`,
|
||||
in2: `foo: bar`,
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "bad-yaml-1",
|
||||
in1: "O#JF*()#",
|
||||
in2: `foo: bar`,
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
desc: "bad-yaml-2",
|
||||
in1: `foo: bar`,
|
||||
in2: "#OHJ*#()F",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
desc: "yaml-not-equal",
|
||||
in1: `zinc: iron
|
||||
stoichiometry: avagadro
|
||||
`,
|
||||
in2: `i-swear: i-am
|
||||
definitely-not: in1
|
||||
`,
|
||||
expect: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got := IsYAMLEqual(tt.in1, tt.in2); got != tt.expect {
|
||||
t.Errorf("%v: got %v want %v", tt.desc, got, tt.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsYAMLEmpty(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
in string
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
desc: "completely-empty",
|
||||
in: "",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "comment-logically-empty",
|
||||
in: `# this is a comment
|
||||
# this is another comment that serves no purpose
|
||||
# (like all comments usually do)
|
||||
`,
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "start-yaml",
|
||||
in: `--- I dont mean anything`,
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "combine-comments-and-yaml",
|
||||
in: `#this is another comment
|
||||
foo: bar
|
||||
# ^ that serves purpose
|
||||
`,
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
desc: "yaml-not-empty",
|
||||
in: `foo: bar`,
|
||||
expect: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
if got := IsYAMLEmpty(tt.in); got != tt.expect {
|
||||
t.Errorf("%v: expect %v got %v", tt.desc, tt.expect, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user