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:
澄潭
2024-08-26 09:51:47 +08:00
committed by GitHub
parent a2c2d1d521
commit f7a419770d
401 changed files with 21171 additions and 7255 deletions

91
hgctl/pkg/util/filter.go Normal file
View 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)
}

View 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)
}
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}
})
}
}