working commit
This commit is contained in:
+181
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
Copyright 2023 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package consistencydetector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
var dataConsistencyDetectionForWatchListEnabled = false
|
||||
|
||||
func init() {
|
||||
dataConsistencyDetectionForWatchListEnabled, _ = strconv.ParseBool(os.Getenv("KUBE_WATCHLIST_INCONSISTENCY_DETECTOR"))
|
||||
}
|
||||
|
||||
// IsDataConsistencyDetectionForWatchListEnabled returns true when
|
||||
// the KUBE_WATCHLIST_INCONSISTENCY_DETECTOR environment variable was set during a binary startup.
|
||||
func IsDataConsistencyDetectionForWatchListEnabled() bool {
|
||||
return dataConsistencyDetectionForWatchListEnabled
|
||||
}
|
||||
|
||||
// SetDataConsistencyDetectionForWatchListEnabledForTest allows to enable/disable data consistency detection for testing purposes.
|
||||
// It returns a function that restores the original value.
|
||||
func SetDataConsistencyDetectionForWatchListEnabledForTest(enabled bool) func() {
|
||||
original := dataConsistencyDetectionForWatchListEnabled
|
||||
dataConsistencyDetectionForWatchListEnabled = enabled
|
||||
return func() {
|
||||
dataConsistencyDetectionForWatchListEnabled = original
|
||||
}
|
||||
}
|
||||
|
||||
type RetrieveItemsFunc[U any] func() []U
|
||||
|
||||
type ListFunc[T runtime.Object] func(ctx context.Context, options metav1.ListOptions) (T, error)
|
||||
|
||||
type TransformFunc func(interface{}) (interface{}, error)
|
||||
|
||||
// CheckDataConsistency exists solely for testing purposes.
|
||||
// we cannot use checkWatchListDataConsistencyIfRequested because
|
||||
// it is guarded by an environmental variable.
|
||||
// we cannot manipulate the environmental variable because
|
||||
// it will affect other tests in this package.
|
||||
func CheckDataConsistency[T runtime.Object, U any](ctx context.Context, identity string, lastSyncedResourceVersion string, listFn ListFunc[T], listItemTransformFunc TransformFunc, listOptions metav1.ListOptions, retrieveItemsFn RetrieveItemsFunc[U]) {
|
||||
if !canFormAdditionalListCall(lastSyncedResourceVersion, listOptions) {
|
||||
klog.V(4).Infof("data consistency check for %s is enabled but the parameters (RV, ListOptions) doesn't allow for creating a valid LIST request. Skipping the data consistency check.", identity)
|
||||
return
|
||||
}
|
||||
klog.Warningf("data consistency check for %s is enabled, this will result in an additional call to the API server.", identity)
|
||||
|
||||
retrievedItems := toMetaObjectSliceOrDie(retrieveItemsFn())
|
||||
listOptions = prepareListCallOptions(lastSyncedResourceVersion, listOptions, len(retrievedItems))
|
||||
var list runtime.Object
|
||||
err := wait.PollUntilContextCancel(ctx, time.Second, true, func(_ context.Context) (done bool, err error) {
|
||||
list, err = listFn(ctx, listOptions)
|
||||
if err != nil {
|
||||
// the consistency check will only be enabled in the CI
|
||||
// and LIST calls in general will be retired by the client-go library
|
||||
// if we fail simply log and retry
|
||||
klog.Errorf("failed to list data from the server, retrying until stopCh is closed, err: %v", err)
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
if err != nil {
|
||||
klog.Errorf("failed to list data from the server, the data consistency check for %s won't be performed, stopCh was closed, err: %v", identity, err)
|
||||
return
|
||||
}
|
||||
|
||||
rawListItems, err := meta.ExtractListWithAlloc(list)
|
||||
if err != nil {
|
||||
panic(err) // this should never happen
|
||||
}
|
||||
if listItemTransformFunc != nil {
|
||||
for i := range rawListItems {
|
||||
obj, err := listItemTransformFunc(rawListItems[i])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
rawListItems[i] = obj.(runtime.Object)
|
||||
}
|
||||
}
|
||||
listItems := toMetaObjectSliceOrDie(rawListItems)
|
||||
|
||||
sort.Sort(byUID(listItems))
|
||||
sort.Sort(byUID(retrievedItems))
|
||||
|
||||
if !reflect.DeepEqual(listItems, retrievedItems) {
|
||||
klog.Infof("previously received data for %s is different than received by the standard list api call against etcd, diff: %v", identity, diff.Diff(listItems, retrievedItems))
|
||||
msg := fmt.Sprintf("data inconsistency detected for %s, panicking!", identity)
|
||||
panic(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// canFormAdditionalListCall ensures that we can form a valid LIST requests
|
||||
// for checking data consistency.
|
||||
func canFormAdditionalListCall(lastSyncedResourceVersion string, listOptions metav1.ListOptions) bool {
|
||||
// since we are setting ResourceVersionMatch to metav1.ResourceVersionMatchExact
|
||||
// we need to make sure that the continuation hasn't been set
|
||||
// https://github.com/kubernetes/kubernetes/blob/be4afb9ef90b19ccb6f7e595cbdb247e088b2347/staging/src/k8s.io/apimachinery/pkg/apis/meta/internalversion/validation/validation.go#L38
|
||||
if len(listOptions.Continue) > 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// since we are setting ResourceVersionMatch to metav1.ResourceVersionMatchExact
|
||||
// we need to make sure that the RV is valid because the validation code forbids RV == "0"
|
||||
// https://github.com/kubernetes/kubernetes/blob/be4afb9ef90b19ccb6f7e595cbdb247e088b2347/staging/src/k8s.io/apimachinery/pkg/apis/meta/internalversion/validation/validation.go#L44
|
||||
if lastSyncedResourceVersion == "0" {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// prepareListCallOptions changes the input list options so that
|
||||
// the list call goes directly to etcd
|
||||
func prepareListCallOptions(lastSyncedResourceVersion string, listOptions metav1.ListOptions, retrievedItemsCount int) metav1.ListOptions {
|
||||
// this is our legacy case:
|
||||
//
|
||||
// the watch cache skips the Limit if the ResourceVersion was set to "0"
|
||||
// thus, to compare with data retrieved directly from etcd
|
||||
// we need to skip the limit to for the list call as well.
|
||||
//
|
||||
// note that when the number of retrieved items is less than the request limit,
|
||||
// it means either the watch cache is disabled, or there is not enough data.
|
||||
// in both cases, we can use the limit because we will be able to compare
|
||||
// the data with the items retrieved from etcd.
|
||||
if listOptions.ResourceVersion == "0" && listOptions.Limit > 0 && int64(retrievedItemsCount) > listOptions.Limit {
|
||||
listOptions.Limit = 0
|
||||
}
|
||||
|
||||
// set the RV and RVM so that we get the snapshot of data
|
||||
// directly from etcd.
|
||||
listOptions.ResourceVersion = lastSyncedResourceVersion
|
||||
listOptions.ResourceVersionMatch = metav1.ResourceVersionMatchExact
|
||||
|
||||
return listOptions
|
||||
}
|
||||
|
||||
type byUID []metav1.Object
|
||||
|
||||
func (a byUID) Len() int { return len(a) }
|
||||
func (a byUID) Less(i, j int) bool { return a[i].GetUID() < a[j].GetUID() }
|
||||
func (a byUID) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
|
||||
func toMetaObjectSliceOrDie[T any](s []T) []metav1.Object {
|
||||
result := make([]metav1.Object, len(s))
|
||||
for i, v := range s {
|
||||
m, err := meta.Accessor(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
result[i] = m
|
||||
}
|
||||
return result
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
approvers:
|
||||
- apelisse
|
||||
- alexzielenski
|
||||
reviewers:
|
||||
- apelisse
|
||||
- alexzielenski
|
||||
- KnVerey
|
||||
labels:
|
||||
- sig/api-machinery
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
Copyright 2024 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package csaupgrade
|
||||
|
||||
type Option func(*options)
|
||||
|
||||
// Subresource set the subresource to upgrade from CSA to SSA.
|
||||
func Subresource(s string) Option {
|
||||
return func(opts *options) {
|
||||
opts.subresource = s
|
||||
}
|
||||
}
|
||||
|
||||
type options struct {
|
||||
subresource string
|
||||
}
|
||||
+334
@@ -0,0 +1,334 @@
|
||||
/*
|
||||
Copyright 2022 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package csaupgrade
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
)
|
||||
|
||||
// Finds all managed fields owners of the given operation type which owns all of
|
||||
// the fields in the given set
|
||||
//
|
||||
// If there is an error decoding one of the fieldsets for any reason, it is ignored
|
||||
// and assumed not to match the query.
|
||||
func FindFieldsOwners(
|
||||
managedFields []metav1.ManagedFieldsEntry,
|
||||
operation metav1.ManagedFieldsOperationType,
|
||||
fields *fieldpath.Set,
|
||||
) []metav1.ManagedFieldsEntry {
|
||||
var result []metav1.ManagedFieldsEntry
|
||||
for _, entry := range managedFields {
|
||||
if entry.Operation != operation {
|
||||
continue
|
||||
}
|
||||
|
||||
fieldSet, err := decodeManagedFieldsEntrySet(entry)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if fields.Difference(&fieldSet).Empty() {
|
||||
result = append(result, entry)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Upgrades the Manager information for fields managed with client-side-apply (CSA)
|
||||
// Prepares fields owned by `csaManager` for 'Update' operations for use now
|
||||
// with the given `ssaManager` for `Apply` operations.
|
||||
//
|
||||
// This transformation should be performed on an object if it has been previously
|
||||
// managed using client-side-apply to prepare it for future use with
|
||||
// server-side-apply.
|
||||
//
|
||||
// Caveats:
|
||||
// 1. This operation is not reversible. Information about which fields the client
|
||||
// owned will be lost in this operation.
|
||||
// 2. Supports being performed either before or after initial server-side apply.
|
||||
// 3. Client-side apply tends to own more fields (including fields that are defaulted),
|
||||
// this will possibly remove this defaults, they will be re-defaulted, that's fine.
|
||||
// 4. Care must be taken to not overwrite the managed fields on the server if they
|
||||
// have changed before sending a patch.
|
||||
//
|
||||
// obj - Target of the operation which has been managed with CSA in the past
|
||||
// csaManagerNames - Names of FieldManagers to merge into ssaManagerName
|
||||
// ssaManagerName - Name of FieldManager to be used for `Apply` operations
|
||||
func UpgradeManagedFields(
|
||||
obj runtime.Object,
|
||||
csaManagerNames sets.Set[string],
|
||||
ssaManagerName string,
|
||||
opts ...Option,
|
||||
) error {
|
||||
o := options{}
|
||||
for _, opt := range opts {
|
||||
opt(&o)
|
||||
}
|
||||
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filteredManagers := accessor.GetManagedFields()
|
||||
|
||||
for csaManagerName := range csaManagerNames {
|
||||
filteredManagers, err = upgradedManagedFields(
|
||||
filteredManagers, csaManagerName, ssaManagerName, o)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Commit changes to object
|
||||
accessor.SetManagedFields(filteredManagers)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Calculates a minimal JSON Patch to send to upgrade managed fields
|
||||
// See `UpgradeManagedFields` for more information.
|
||||
//
|
||||
// obj - Target of the operation which has been managed with CSA in the past
|
||||
// csaManagerNames - Names of FieldManagers to merge into ssaManagerName
|
||||
// ssaManagerName - Name of FieldManager to be used for `Apply` operations
|
||||
//
|
||||
// Returns non-nil error if there was an error, a JSON patch, or nil bytes if
|
||||
// there is no work to be done.
|
||||
func UpgradeManagedFieldsPatch(
|
||||
obj runtime.Object,
|
||||
csaManagerNames sets.Set[string],
|
||||
ssaManagerName string,
|
||||
opts ...Option,
|
||||
) ([]byte, error) {
|
||||
o := options{}
|
||||
for _, opt := range opts {
|
||||
opt(&o)
|
||||
}
|
||||
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
managedFields := accessor.GetManagedFields()
|
||||
filteredManagers := accessor.GetManagedFields()
|
||||
for csaManagerName := range csaManagerNames {
|
||||
filteredManagers, err = upgradedManagedFields(
|
||||
filteredManagers, csaManagerName, ssaManagerName, o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(managedFields, filteredManagers) {
|
||||
// If the managed fields have not changed from the transformed version,
|
||||
// there is no patch to perform
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Create a patch with a diff between old and new objects.
|
||||
// Just include all managed fields since that is only thing that will change
|
||||
//
|
||||
// Also include test for RV to avoid race condition
|
||||
jsonPatch := []map[string]interface{}{
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/metadata/managedFields",
|
||||
"value": filteredManagers,
|
||||
},
|
||||
{
|
||||
// Use "replace" instead of "test" operation so that etcd rejects with
|
||||
// 409 conflict instead of apiserver with an invalid request
|
||||
"op": "replace",
|
||||
"path": "/metadata/resourceVersion",
|
||||
"value": accessor.GetResourceVersion(),
|
||||
},
|
||||
}
|
||||
|
||||
return json.Marshal(jsonPatch)
|
||||
}
|
||||
|
||||
// Returns a copy of the provided managed fields that has been migrated from
|
||||
// client-side-apply to server-side-apply, or an error if there was an issue
|
||||
func upgradedManagedFields(
|
||||
managedFields []metav1.ManagedFieldsEntry,
|
||||
csaManagerName string,
|
||||
ssaManagerName string,
|
||||
opts options,
|
||||
) ([]metav1.ManagedFieldsEntry, error) {
|
||||
if managedFields == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Create managed fields clone since we modify the values
|
||||
managedFieldsCopy := make([]metav1.ManagedFieldsEntry, len(managedFields))
|
||||
if copy(managedFieldsCopy, managedFields) != len(managedFields) {
|
||||
return nil, errors.New("failed to copy managed fields")
|
||||
}
|
||||
managedFields = managedFieldsCopy
|
||||
|
||||
// Locate SSA manager
|
||||
replaceIndex, managerExists := findFirstIndex(managedFields,
|
||||
func(entry metav1.ManagedFieldsEntry) bool {
|
||||
return entry.Manager == ssaManagerName &&
|
||||
entry.Operation == metav1.ManagedFieldsOperationApply &&
|
||||
entry.Subresource == opts.subresource
|
||||
})
|
||||
|
||||
if !managerExists {
|
||||
// SSA manager does not exist. Find the most recent matching CSA manager,
|
||||
// convert it to an SSA manager.
|
||||
//
|
||||
// (find first index, since managed fields are sorted so that most recent is
|
||||
// first in the list)
|
||||
replaceIndex, managerExists = findFirstIndex(managedFields,
|
||||
func(entry metav1.ManagedFieldsEntry) bool {
|
||||
return entry.Manager == csaManagerName &&
|
||||
entry.Operation == metav1.ManagedFieldsOperationUpdate &&
|
||||
entry.Subresource == opts.subresource
|
||||
})
|
||||
|
||||
if !managerExists {
|
||||
// There are no CSA managers that need to be converted. Nothing to do
|
||||
// Return early
|
||||
return managedFields, nil
|
||||
}
|
||||
|
||||
// Convert CSA manager into SSA manager
|
||||
managedFields[replaceIndex].Operation = metav1.ManagedFieldsOperationApply
|
||||
managedFields[replaceIndex].Manager = ssaManagerName
|
||||
}
|
||||
err := unionManagerIntoIndex(managedFields, replaceIndex, csaManagerName, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create version of managed fields which has no CSA managers with the given name
|
||||
filteredManagers := filter(managedFields, func(entry metav1.ManagedFieldsEntry) bool {
|
||||
return !(entry.Manager == csaManagerName &&
|
||||
entry.Operation == metav1.ManagedFieldsOperationUpdate &&
|
||||
entry.Subresource == opts.subresource)
|
||||
})
|
||||
|
||||
return filteredManagers, nil
|
||||
}
|
||||
|
||||
// Locates an Update manager entry named `csaManagerName` with the same APIVersion
|
||||
// as the manager at the targetIndex. Unions both manager's fields together
|
||||
// into the manager specified by `targetIndex`. No other managers are modified.
|
||||
func unionManagerIntoIndex(
|
||||
entries []metav1.ManagedFieldsEntry,
|
||||
targetIndex int,
|
||||
csaManagerName string,
|
||||
opts options,
|
||||
) error {
|
||||
ssaManager := entries[targetIndex]
|
||||
|
||||
// find Update manager of same APIVersion, union ssa fields with it.
|
||||
// discard all other Update managers of the same name
|
||||
csaManagerIndex, csaManagerExists := findFirstIndex(entries,
|
||||
func(entry metav1.ManagedFieldsEntry) bool {
|
||||
return entry.Manager == csaManagerName &&
|
||||
entry.Operation == metav1.ManagedFieldsOperationUpdate &&
|
||||
entry.Subresource == opts.subresource &&
|
||||
entry.APIVersion == ssaManager.APIVersion
|
||||
})
|
||||
|
||||
targetFieldSet, err := decodeManagedFieldsEntrySet(ssaManager)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert fields to set: %w", err)
|
||||
}
|
||||
|
||||
combinedFieldSet := &targetFieldSet
|
||||
|
||||
// Union the csa manager with the existing SSA manager. Do nothing if
|
||||
// there was no good candidate found
|
||||
if csaManagerExists {
|
||||
csaManager := entries[csaManagerIndex]
|
||||
|
||||
csaFieldSet, err := decodeManagedFieldsEntrySet(csaManager)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert fields to set: %w", err)
|
||||
}
|
||||
|
||||
combinedFieldSet = combinedFieldSet.Union(&csaFieldSet)
|
||||
}
|
||||
|
||||
// Encode the fields back to the serialized format
|
||||
err = encodeManagedFieldsEntrySet(&entries[targetIndex], *combinedFieldSet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode field set: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findFirstIndex[T any](
|
||||
collection []T,
|
||||
predicate func(T) bool,
|
||||
) (int, bool) {
|
||||
for idx, entry := range collection {
|
||||
if predicate(entry) {
|
||||
return idx, true
|
||||
}
|
||||
}
|
||||
|
||||
return -1, false
|
||||
}
|
||||
|
||||
func filter[T any](
|
||||
collection []T,
|
||||
predicate func(T) bool,
|
||||
) []T {
|
||||
result := make([]T, 0, len(collection))
|
||||
|
||||
for _, value := range collection {
|
||||
if predicate(value) {
|
||||
result = append(result, value)
|
||||
}
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Included from fieldmanager.internal to avoid dependency cycle
|
||||
// FieldsToSet creates a set paths from an input trie of fields
|
||||
func decodeManagedFieldsEntrySet(f metav1.ManagedFieldsEntry) (s fieldpath.Set, err error) {
|
||||
err = s.FromJSON(bytes.NewReader(f.FieldsV1.Raw))
|
||||
return s, err
|
||||
}
|
||||
|
||||
// SetToFields creates a trie of fields from an input set of paths
|
||||
func encodeManagedFieldsEntrySet(f *metav1.ManagedFieldsEntry, s fieldpath.Set) (err error) {
|
||||
f.FieldsV1.Raw, err = s.ToJSON()
|
||||
return err
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package homedir
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// HomeDir returns the home directory for the current user.
|
||||
// On Windows:
|
||||
// 1. the first of %HOME%, %HOMEDRIVE%%HOMEPATH%, %USERPROFILE% containing a `.kube\config` file is returned.
|
||||
// 2. if none of those locations contain a `.kube\config` file, the first of %HOME%, %USERPROFILE%, %HOMEDRIVE%%HOMEPATH% that exists and is writeable is returned.
|
||||
// 3. if none of those locations are writeable, the first of %HOME%, %USERPROFILE%, %HOMEDRIVE%%HOMEPATH% that exists is returned.
|
||||
// 4. if none of those locations exists, the first of %HOME%, %USERPROFILE%, %HOMEDRIVE%%HOMEPATH% that is set is returned.
|
||||
func HomeDir() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
home := os.Getenv("HOME")
|
||||
homeDriveHomePath := ""
|
||||
if homeDrive, homePath := os.Getenv("HOMEDRIVE"), os.Getenv("HOMEPATH"); len(homeDrive) > 0 && len(homePath) > 0 {
|
||||
homeDriveHomePath = homeDrive + homePath
|
||||
}
|
||||
userProfile := os.Getenv("USERPROFILE")
|
||||
|
||||
// Return first of %HOME%, %HOMEDRIVE%/%HOMEPATH%, %USERPROFILE% that contains a `.kube\config` file.
|
||||
// %HOMEDRIVE%/%HOMEPATH% is preferred over %USERPROFILE% for backwards-compatibility.
|
||||
for _, p := range []string{home, homeDriveHomePath, userProfile} {
|
||||
if len(p) == 0 {
|
||||
continue
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(p, ".kube", "config")); err != nil {
|
||||
continue
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
firstSetPath := ""
|
||||
firstExistingPath := ""
|
||||
|
||||
// Prefer %USERPROFILE% over %HOMEDRIVE%/%HOMEPATH% for compatibility with other auth-writing tools
|
||||
for _, p := range []string{home, userProfile, homeDriveHomePath} {
|
||||
if len(p) == 0 {
|
||||
continue
|
||||
}
|
||||
if len(firstSetPath) == 0 {
|
||||
// remember the first path that is set
|
||||
firstSetPath = p
|
||||
}
|
||||
info, err := os.Stat(p)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if len(firstExistingPath) == 0 {
|
||||
// remember the first path that exists
|
||||
firstExistingPath = p
|
||||
}
|
||||
if info.IsDir() && info.Mode().Perm()&(1<<(uint(7))) != 0 {
|
||||
// return first path that is writeable
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
// If none are writeable, return first location that exists
|
||||
if len(firstExistingPath) > 0 {
|
||||
return firstExistingPath
|
||||
}
|
||||
|
||||
// If none exist, return first location that is set
|
||||
if len(firstSetPath) > 0 {
|
||||
return firstSetPath
|
||||
}
|
||||
|
||||
// We've got nothing
|
||||
return ""
|
||||
}
|
||||
return os.Getenv("HOME")
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
Copyright 2015 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// package jsonpath is a template engine using jsonpath syntax,
|
||||
// which can be seen at http://goessner.net/articles/JsonPath/.
|
||||
// In addition, it has {range} {end} function to iterate list and slice.
|
||||
package jsonpath
|
||||
+582
@@ -0,0 +1,582 @@
|
||||
/*
|
||||
Copyright 2015 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package jsonpath
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"k8s.io/client-go/third_party/forked/golang/template"
|
||||
)
|
||||
|
||||
type JSONPath struct {
|
||||
name string
|
||||
parser *Parser
|
||||
beginRange int
|
||||
inRange int
|
||||
endRange int
|
||||
|
||||
lastEndNode *Node
|
||||
|
||||
allowMissingKeys bool
|
||||
outputJSON bool
|
||||
}
|
||||
|
||||
// New creates a new JSONPath with the given name.
|
||||
func New(name string) *JSONPath {
|
||||
return &JSONPath{
|
||||
name: name,
|
||||
beginRange: 0,
|
||||
inRange: 0,
|
||||
endRange: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// AllowMissingKeys allows a caller to specify whether they want an error if a field or map key
|
||||
// cannot be located, or simply an empty result. The receiver is returned for chaining.
|
||||
func (j *JSONPath) AllowMissingKeys(allow bool) *JSONPath {
|
||||
j.allowMissingKeys = allow
|
||||
return j
|
||||
}
|
||||
|
||||
// Parse parses the given template and returns an error.
|
||||
func (j *JSONPath) Parse(text string) error {
|
||||
var err error
|
||||
j.parser, err = Parse(j.name, text)
|
||||
return err
|
||||
}
|
||||
|
||||
// Execute bounds data into template and writes the result.
|
||||
func (j *JSONPath) Execute(wr io.Writer, data interface{}) error {
|
||||
fullResults, err := j.FindResults(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for ix := range fullResults {
|
||||
if err := j.PrintResults(wr, fullResults[ix]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *JSONPath) FindResults(data interface{}) ([][]reflect.Value, error) {
|
||||
if j.parser == nil {
|
||||
return nil, fmt.Errorf("%s is an incomplete jsonpath template", j.name)
|
||||
}
|
||||
|
||||
cur := []reflect.Value{reflect.ValueOf(data)}
|
||||
nodes := j.parser.Root.Nodes
|
||||
fullResult := [][]reflect.Value{}
|
||||
for i := 0; i < len(nodes); i++ {
|
||||
node := nodes[i]
|
||||
results, err := j.walk(cur, node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// encounter an end node, break the current block
|
||||
if j.endRange > 0 && j.endRange <= j.inRange {
|
||||
j.endRange--
|
||||
j.lastEndNode = &nodes[i]
|
||||
break
|
||||
}
|
||||
// encounter a range node, start a range loop
|
||||
if j.beginRange > 0 {
|
||||
j.beginRange--
|
||||
j.inRange++
|
||||
if len(results) > 0 {
|
||||
for _, value := range results {
|
||||
j.parser.Root.Nodes = nodes[i+1:]
|
||||
nextResults, err := j.FindResults(value.Interface())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fullResult = append(fullResult, nextResults...)
|
||||
}
|
||||
} else {
|
||||
// If the range has no results, we still need to process the nodes within the range
|
||||
// so the position will advance to the end node
|
||||
j.parser.Root.Nodes = nodes[i+1:]
|
||||
_, err := j.FindResults(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
j.inRange--
|
||||
|
||||
// Fast forward to resume processing after the most recent end node that was encountered
|
||||
for k := i + 1; k < len(nodes); k++ {
|
||||
if &nodes[k] == j.lastEndNode {
|
||||
i = k
|
||||
break
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
fullResult = append(fullResult, results)
|
||||
}
|
||||
return fullResult, nil
|
||||
}
|
||||
|
||||
// EnableJSONOutput changes the PrintResults behavior to return a JSON array of results
|
||||
func (j *JSONPath) EnableJSONOutput(v bool) {
|
||||
j.outputJSON = v
|
||||
}
|
||||
|
||||
// PrintResults writes the results into writer
|
||||
func (j *JSONPath) PrintResults(wr io.Writer, results []reflect.Value) error {
|
||||
if j.outputJSON {
|
||||
// convert the []reflect.Value to something that json
|
||||
// will be able to marshal
|
||||
r := make([]interface{}, 0, len(results))
|
||||
for i := range results {
|
||||
r = append(r, results[i].Interface())
|
||||
}
|
||||
results = []reflect.Value{reflect.ValueOf(r)}
|
||||
}
|
||||
for i, r := range results {
|
||||
var text []byte
|
||||
var err error
|
||||
outputJSON := true
|
||||
kind := r.Kind()
|
||||
if kind == reflect.Interface {
|
||||
kind = r.Elem().Kind()
|
||||
}
|
||||
switch kind {
|
||||
case reflect.Map:
|
||||
case reflect.Array:
|
||||
case reflect.Slice:
|
||||
case reflect.Struct:
|
||||
default:
|
||||
outputJSON = false
|
||||
}
|
||||
switch {
|
||||
case outputJSON || j.outputJSON:
|
||||
if j.outputJSON {
|
||||
text, err = json.MarshalIndent(r.Interface(), "", " ")
|
||||
text = append(text, '\n')
|
||||
} else {
|
||||
text, err = json.Marshal(r.Interface())
|
||||
}
|
||||
default:
|
||||
text, err = j.evalToText(r)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if i != len(results)-1 {
|
||||
text = append(text, ' ')
|
||||
}
|
||||
if _, err = wr.Write(text); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// walk visits tree rooted at the given node in DFS order
|
||||
func (j *JSONPath) walk(value []reflect.Value, node Node) ([]reflect.Value, error) {
|
||||
switch node := node.(type) {
|
||||
case *ListNode:
|
||||
return j.evalList(value, node)
|
||||
case *TextNode:
|
||||
return []reflect.Value{reflect.ValueOf(node.Text)}, nil
|
||||
case *FieldNode:
|
||||
return j.evalField(value, node)
|
||||
case *ArrayNode:
|
||||
return j.evalArray(value, node)
|
||||
case *FilterNode:
|
||||
return j.evalFilter(value, node)
|
||||
case *IntNode:
|
||||
return j.evalInt(value, node)
|
||||
case *BoolNode:
|
||||
return j.evalBool(value, node)
|
||||
case *FloatNode:
|
||||
return j.evalFloat(value, node)
|
||||
case *WildcardNode:
|
||||
return j.evalWildcard(value, node)
|
||||
case *RecursiveNode:
|
||||
return j.evalRecursive(value, node)
|
||||
case *UnionNode:
|
||||
return j.evalUnion(value, node)
|
||||
case *IdentifierNode:
|
||||
return j.evalIdentifier(value, node)
|
||||
default:
|
||||
return value, fmt.Errorf("unexpected Node %v", node)
|
||||
}
|
||||
}
|
||||
|
||||
// evalInt evaluates IntNode
|
||||
func (j *JSONPath) evalInt(input []reflect.Value, node *IntNode) ([]reflect.Value, error) {
|
||||
result := make([]reflect.Value, len(input))
|
||||
for i := range input {
|
||||
result[i] = reflect.ValueOf(node.Value)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// evalFloat evaluates FloatNode
|
||||
func (j *JSONPath) evalFloat(input []reflect.Value, node *FloatNode) ([]reflect.Value, error) {
|
||||
result := make([]reflect.Value, len(input))
|
||||
for i := range input {
|
||||
result[i] = reflect.ValueOf(node.Value)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// evalBool evaluates BoolNode
|
||||
func (j *JSONPath) evalBool(input []reflect.Value, node *BoolNode) ([]reflect.Value, error) {
|
||||
result := make([]reflect.Value, len(input))
|
||||
for i := range input {
|
||||
result[i] = reflect.ValueOf(node.Value)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// evalList evaluates ListNode
|
||||
func (j *JSONPath) evalList(value []reflect.Value, node *ListNode) ([]reflect.Value, error) {
|
||||
var err error
|
||||
curValue := value
|
||||
for _, node := range node.Nodes {
|
||||
curValue, err = j.walk(curValue, node)
|
||||
if err != nil {
|
||||
return curValue, err
|
||||
}
|
||||
}
|
||||
return curValue, nil
|
||||
}
|
||||
|
||||
// evalIdentifier evaluates IdentifierNode
|
||||
func (j *JSONPath) evalIdentifier(input []reflect.Value, node *IdentifierNode) ([]reflect.Value, error) {
|
||||
results := []reflect.Value{}
|
||||
switch node.Name {
|
||||
case "range":
|
||||
j.beginRange++
|
||||
results = input
|
||||
case "end":
|
||||
if j.inRange > 0 {
|
||||
j.endRange++
|
||||
} else {
|
||||
return results, fmt.Errorf("not in range, nothing to end")
|
||||
}
|
||||
default:
|
||||
return input, fmt.Errorf("unrecognized identifier %v", node.Name)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// evalArray evaluates ArrayNode
|
||||
func (j *JSONPath) evalArray(input []reflect.Value, node *ArrayNode) ([]reflect.Value, error) {
|
||||
result := []reflect.Value{}
|
||||
for _, value := range input {
|
||||
|
||||
value, isNil := template.Indirect(value)
|
||||
if isNil {
|
||||
continue
|
||||
}
|
||||
if value.Kind() != reflect.Array && value.Kind() != reflect.Slice {
|
||||
return input, fmt.Errorf("%v is not array or slice", value.Type())
|
||||
}
|
||||
params := node.Params
|
||||
if !params[0].Known {
|
||||
params[0].Value = 0
|
||||
}
|
||||
if params[0].Value < 0 {
|
||||
params[0].Value += value.Len()
|
||||
}
|
||||
if !params[1].Known {
|
||||
params[1].Value = value.Len()
|
||||
}
|
||||
|
||||
if params[1].Value < 0 || (params[1].Value == 0 && params[1].Derived) {
|
||||
params[1].Value += value.Len()
|
||||
}
|
||||
sliceLength := value.Len()
|
||||
if params[1].Value != params[0].Value { // if you're requesting zero elements, allow it through.
|
||||
if params[0].Value >= sliceLength || params[0].Value < 0 {
|
||||
return input, fmt.Errorf("array index out of bounds: index %d, length %d", params[0].Value, sliceLength)
|
||||
}
|
||||
if params[1].Value > sliceLength || params[1].Value < 0 {
|
||||
return input, fmt.Errorf("array index out of bounds: index %d, length %d", params[1].Value-1, sliceLength)
|
||||
}
|
||||
if params[0].Value > params[1].Value {
|
||||
return input, fmt.Errorf("starting index %d is greater than ending index %d", params[0].Value, params[1].Value)
|
||||
}
|
||||
} else {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
value = value.Slice(params[0].Value, params[1].Value)
|
||||
|
||||
step := 1
|
||||
if params[2].Known {
|
||||
if params[2].Value <= 0 {
|
||||
return input, fmt.Errorf("step must be > 0")
|
||||
}
|
||||
step = params[2].Value
|
||||
}
|
||||
for i := 0; i < value.Len(); i += step {
|
||||
result = append(result, value.Index(i))
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// evalUnion evaluates UnionNode
|
||||
func (j *JSONPath) evalUnion(input []reflect.Value, node *UnionNode) ([]reflect.Value, error) {
|
||||
result := []reflect.Value{}
|
||||
for _, listNode := range node.Nodes {
|
||||
temp, err := j.evalList(input, listNode)
|
||||
if err != nil {
|
||||
return input, err
|
||||
}
|
||||
result = append(result, temp...)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (j *JSONPath) findFieldInValue(value *reflect.Value, node *FieldNode) (reflect.Value, error) {
|
||||
t := value.Type()
|
||||
var inlineValue *reflect.Value
|
||||
for ix := 0; ix < t.NumField(); ix++ {
|
||||
f := t.Field(ix)
|
||||
jsonTag := f.Tag.Get("json")
|
||||
parts := strings.Split(jsonTag, ",")
|
||||
if len(parts) == 0 {
|
||||
continue
|
||||
}
|
||||
if parts[0] == node.Value {
|
||||
return value.Field(ix), nil
|
||||
}
|
||||
if len(parts[0]) == 0 {
|
||||
val := value.Field(ix)
|
||||
inlineValue = &val
|
||||
}
|
||||
}
|
||||
if inlineValue != nil {
|
||||
if inlineValue.Kind() == reflect.Struct {
|
||||
// handle 'inline'
|
||||
match, err := j.findFieldInValue(inlineValue, node)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
if match.IsValid() {
|
||||
return match, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return value.FieldByName(node.Value), nil
|
||||
}
|
||||
|
||||
// evalField evaluates field of struct or key of map.
|
||||
func (j *JSONPath) evalField(input []reflect.Value, node *FieldNode) ([]reflect.Value, error) {
|
||||
results := []reflect.Value{}
|
||||
// If there's no input, there's no output
|
||||
if len(input) == 0 {
|
||||
return results, nil
|
||||
}
|
||||
for _, value := range input {
|
||||
var result reflect.Value
|
||||
value, isNil := template.Indirect(value)
|
||||
if isNil {
|
||||
continue
|
||||
}
|
||||
|
||||
if value.Kind() == reflect.Struct {
|
||||
var err error
|
||||
if result, err = j.findFieldInValue(&value, node); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if value.Kind() == reflect.Map {
|
||||
mapKeyType := value.Type().Key()
|
||||
nodeValue := reflect.ValueOf(node.Value)
|
||||
// node value type must be convertible to map key type
|
||||
if !nodeValue.Type().ConvertibleTo(mapKeyType) {
|
||||
return results, fmt.Errorf("%s is not convertible to %s", nodeValue, mapKeyType)
|
||||
}
|
||||
result = value.MapIndex(nodeValue.Convert(mapKeyType))
|
||||
}
|
||||
if result.IsValid() {
|
||||
results = append(results, result)
|
||||
}
|
||||
}
|
||||
if len(results) == 0 {
|
||||
if j.allowMissingKeys {
|
||||
return results, nil
|
||||
}
|
||||
return results, fmt.Errorf("%s is not found", node.Value)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// evalWildcard extracts all contents of the given value
|
||||
func (j *JSONPath) evalWildcard(input []reflect.Value, node *WildcardNode) ([]reflect.Value, error) {
|
||||
results := []reflect.Value{}
|
||||
for _, value := range input {
|
||||
value, isNil := template.Indirect(value)
|
||||
if isNil {
|
||||
continue
|
||||
}
|
||||
|
||||
kind := value.Kind()
|
||||
if kind == reflect.Struct {
|
||||
for i := 0; i < value.NumField(); i++ {
|
||||
results = append(results, value.Field(i))
|
||||
}
|
||||
} else if kind == reflect.Map {
|
||||
for _, key := range value.MapKeys() {
|
||||
results = append(results, value.MapIndex(key))
|
||||
}
|
||||
} else if kind == reflect.Array || kind == reflect.Slice || kind == reflect.String {
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
results = append(results, value.Index(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// evalRecursive visits the given value recursively and pushes all of them to result
|
||||
func (j *JSONPath) evalRecursive(input []reflect.Value, node *RecursiveNode) ([]reflect.Value, error) {
|
||||
result := []reflect.Value{}
|
||||
for _, value := range input {
|
||||
results := []reflect.Value{}
|
||||
value, isNil := template.Indirect(value)
|
||||
if isNil {
|
||||
continue
|
||||
}
|
||||
|
||||
kind := value.Kind()
|
||||
if kind == reflect.Struct {
|
||||
for i := 0; i < value.NumField(); i++ {
|
||||
results = append(results, value.Field(i))
|
||||
}
|
||||
} else if kind == reflect.Map {
|
||||
for _, key := range value.MapKeys() {
|
||||
results = append(results, value.MapIndex(key))
|
||||
}
|
||||
} else if kind == reflect.Array || kind == reflect.Slice || kind == reflect.String {
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
results = append(results, value.Index(i))
|
||||
}
|
||||
}
|
||||
if len(results) != 0 {
|
||||
result = append(result, value)
|
||||
output, err := j.evalRecursive(results, node)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
result = append(result, output...)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// evalFilter filters array according to FilterNode
|
||||
func (j *JSONPath) evalFilter(input []reflect.Value, node *FilterNode) ([]reflect.Value, error) {
|
||||
results := []reflect.Value{}
|
||||
for _, value := range input {
|
||||
value, _ = template.Indirect(value)
|
||||
|
||||
if value.Kind() != reflect.Array && value.Kind() != reflect.Slice {
|
||||
return input, fmt.Errorf("%v is not array or slice and cannot be filtered", value)
|
||||
}
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
temp := []reflect.Value{value.Index(i)}
|
||||
lefts, err := j.evalList(temp, node.Left)
|
||||
|
||||
//case exists
|
||||
if node.Operator == "exists" {
|
||||
if len(lefts) > 0 {
|
||||
results = append(results, value.Index(i))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return input, err
|
||||
}
|
||||
|
||||
var left, right interface{}
|
||||
switch {
|
||||
case len(lefts) == 0:
|
||||
continue
|
||||
case len(lefts) > 1:
|
||||
return input, fmt.Errorf("can only compare one element at a time")
|
||||
}
|
||||
left = lefts[0].Interface()
|
||||
|
||||
rights, err := j.evalList(temp, node.Right)
|
||||
if err != nil {
|
||||
return input, err
|
||||
}
|
||||
switch {
|
||||
case len(rights) == 0:
|
||||
continue
|
||||
case len(rights) > 1:
|
||||
return input, fmt.Errorf("can only compare one element at a time")
|
||||
}
|
||||
right = rights[0].Interface()
|
||||
|
||||
pass := false
|
||||
switch node.Operator {
|
||||
case "<":
|
||||
pass, err = template.Less(left, right)
|
||||
case ">":
|
||||
pass, err = template.Greater(left, right)
|
||||
case "==":
|
||||
pass, err = template.Equal(left, right)
|
||||
case "!=":
|
||||
pass, err = template.NotEqual(left, right)
|
||||
case "<=":
|
||||
pass, err = template.LessEqual(left, right)
|
||||
case ">=":
|
||||
pass, err = template.GreaterEqual(left, right)
|
||||
default:
|
||||
return results, fmt.Errorf("unrecognized filter operator %s", node.Operator)
|
||||
}
|
||||
if err != nil {
|
||||
return results, err
|
||||
}
|
||||
if pass {
|
||||
results = append(results, value.Index(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// evalToText translates reflect value to corresponding text
|
||||
func (j *JSONPath) evalToText(v reflect.Value) ([]byte, error) {
|
||||
iface, ok := template.PrintableValue(v)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("can't print type %s", v.Type())
|
||||
}
|
||||
if iface == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
fmt.Fprint(&buffer, iface)
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
Copyright 2015 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package jsonpath
|
||||
|
||||
import "fmt"
|
||||
|
||||
// NodeType identifies the type of a parse tree node.
|
||||
type NodeType int
|
||||
|
||||
// Type returns itself and provides an easy default implementation
|
||||
func (t NodeType) Type() NodeType {
|
||||
return t
|
||||
}
|
||||
|
||||
func (t NodeType) String() string {
|
||||
return NodeTypeName[t]
|
||||
}
|
||||
|
||||
const (
|
||||
NodeText NodeType = iota
|
||||
NodeArray
|
||||
NodeList
|
||||
NodeField
|
||||
NodeIdentifier
|
||||
NodeFilter
|
||||
NodeInt
|
||||
NodeFloat
|
||||
NodeWildcard
|
||||
NodeRecursive
|
||||
NodeUnion
|
||||
NodeBool
|
||||
)
|
||||
|
||||
var NodeTypeName = map[NodeType]string{
|
||||
NodeText: "NodeText",
|
||||
NodeArray: "NodeArray",
|
||||
NodeList: "NodeList",
|
||||
NodeField: "NodeField",
|
||||
NodeIdentifier: "NodeIdentifier",
|
||||
NodeFilter: "NodeFilter",
|
||||
NodeInt: "NodeInt",
|
||||
NodeFloat: "NodeFloat",
|
||||
NodeWildcard: "NodeWildcard",
|
||||
NodeRecursive: "NodeRecursive",
|
||||
NodeUnion: "NodeUnion",
|
||||
NodeBool: "NodeBool",
|
||||
}
|
||||
|
||||
type Node interface {
|
||||
Type() NodeType
|
||||
String() string
|
||||
}
|
||||
|
||||
// ListNode holds a sequence of nodes.
|
||||
type ListNode struct {
|
||||
NodeType
|
||||
Nodes []Node // The element nodes in lexical order.
|
||||
}
|
||||
|
||||
func newList() *ListNode {
|
||||
return &ListNode{NodeType: NodeList}
|
||||
}
|
||||
|
||||
func (l *ListNode) append(n Node) {
|
||||
l.Nodes = append(l.Nodes, n)
|
||||
}
|
||||
|
||||
func (l *ListNode) String() string {
|
||||
return l.Type().String()
|
||||
}
|
||||
|
||||
// TextNode holds plain text.
|
||||
type TextNode struct {
|
||||
NodeType
|
||||
Text string // The text; may span newlines.
|
||||
}
|
||||
|
||||
func newText(text string) *TextNode {
|
||||
return &TextNode{NodeType: NodeText, Text: text}
|
||||
}
|
||||
|
||||
func (t *TextNode) String() string {
|
||||
return fmt.Sprintf("%s: %s", t.Type(), t.Text)
|
||||
}
|
||||
|
||||
// FieldNode holds field of struct
|
||||
type FieldNode struct {
|
||||
NodeType
|
||||
Value string
|
||||
}
|
||||
|
||||
func newField(value string) *FieldNode {
|
||||
return &FieldNode{NodeType: NodeField, Value: value}
|
||||
}
|
||||
|
||||
func (f *FieldNode) String() string {
|
||||
return fmt.Sprintf("%s: %s", f.Type(), f.Value)
|
||||
}
|
||||
|
||||
// IdentifierNode holds an identifier
|
||||
type IdentifierNode struct {
|
||||
NodeType
|
||||
Name string
|
||||
}
|
||||
|
||||
func newIdentifier(value string) *IdentifierNode {
|
||||
return &IdentifierNode{
|
||||
NodeType: NodeIdentifier,
|
||||
Name: value,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *IdentifierNode) String() string {
|
||||
return fmt.Sprintf("%s: %s", f.Type(), f.Name)
|
||||
}
|
||||
|
||||
// ParamsEntry holds param information for ArrayNode
|
||||
type ParamsEntry struct {
|
||||
Value int
|
||||
Known bool // whether the value is known when parse it
|
||||
Derived bool
|
||||
}
|
||||
|
||||
// ArrayNode holds start, end, step information for array index selection
|
||||
type ArrayNode struct {
|
||||
NodeType
|
||||
Params [3]ParamsEntry // start, end, step
|
||||
}
|
||||
|
||||
func newArray(params [3]ParamsEntry) *ArrayNode {
|
||||
return &ArrayNode{
|
||||
NodeType: NodeArray,
|
||||
Params: params,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ArrayNode) String() string {
|
||||
return fmt.Sprintf("%s: %v", a.Type(), a.Params)
|
||||
}
|
||||
|
||||
// FilterNode holds operand and operator information for filter
|
||||
type FilterNode struct {
|
||||
NodeType
|
||||
Left *ListNode
|
||||
Right *ListNode
|
||||
Operator string
|
||||
}
|
||||
|
||||
func newFilter(left, right *ListNode, operator string) *FilterNode {
|
||||
return &FilterNode{
|
||||
NodeType: NodeFilter,
|
||||
Left: left,
|
||||
Right: right,
|
||||
Operator: operator,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FilterNode) String() string {
|
||||
return fmt.Sprintf("%s: %s %s %s", f.Type(), f.Left, f.Operator, f.Right)
|
||||
}
|
||||
|
||||
// IntNode holds integer value
|
||||
type IntNode struct {
|
||||
NodeType
|
||||
Value int
|
||||
}
|
||||
|
||||
func newInt(num int) *IntNode {
|
||||
return &IntNode{NodeType: NodeInt, Value: num}
|
||||
}
|
||||
|
||||
func (i *IntNode) String() string {
|
||||
return fmt.Sprintf("%s: %d", i.Type(), i.Value)
|
||||
}
|
||||
|
||||
// FloatNode holds float value
|
||||
type FloatNode struct {
|
||||
NodeType
|
||||
Value float64
|
||||
}
|
||||
|
||||
func newFloat(num float64) *FloatNode {
|
||||
return &FloatNode{NodeType: NodeFloat, Value: num}
|
||||
}
|
||||
|
||||
func (i *FloatNode) String() string {
|
||||
return fmt.Sprintf("%s: %f", i.Type(), i.Value)
|
||||
}
|
||||
|
||||
// WildcardNode means a wildcard
|
||||
type WildcardNode struct {
|
||||
NodeType
|
||||
}
|
||||
|
||||
func newWildcard() *WildcardNode {
|
||||
return &WildcardNode{NodeType: NodeWildcard}
|
||||
}
|
||||
|
||||
func (i *WildcardNode) String() string {
|
||||
return i.Type().String()
|
||||
}
|
||||
|
||||
// RecursiveNode means a recursive descent operator
|
||||
type RecursiveNode struct {
|
||||
NodeType
|
||||
}
|
||||
|
||||
func newRecursive() *RecursiveNode {
|
||||
return &RecursiveNode{NodeType: NodeRecursive}
|
||||
}
|
||||
|
||||
func (r *RecursiveNode) String() string {
|
||||
return r.Type().String()
|
||||
}
|
||||
|
||||
// UnionNode is union of ListNode
|
||||
type UnionNode struct {
|
||||
NodeType
|
||||
Nodes []*ListNode
|
||||
}
|
||||
|
||||
func newUnion(nodes []*ListNode) *UnionNode {
|
||||
return &UnionNode{NodeType: NodeUnion, Nodes: nodes}
|
||||
}
|
||||
|
||||
func (u *UnionNode) String() string {
|
||||
return u.Type().String()
|
||||
}
|
||||
|
||||
// BoolNode holds bool value
|
||||
type BoolNode struct {
|
||||
NodeType
|
||||
Value bool
|
||||
}
|
||||
|
||||
func newBool(value bool) *BoolNode {
|
||||
return &BoolNode{NodeType: NodeBool, Value: value}
|
||||
}
|
||||
|
||||
func (b *BoolNode) String() string {
|
||||
return fmt.Sprintf("%s: %t", b.Type(), b.Value)
|
||||
}
|
||||
+527
@@ -0,0 +1,527 @@
|
||||
/*
|
||||
Copyright 2015 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package jsonpath
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const eof = -1
|
||||
|
||||
const (
|
||||
leftDelim = "{"
|
||||
rightDelim = "}"
|
||||
)
|
||||
|
||||
type Parser struct {
|
||||
Name string
|
||||
Root *ListNode
|
||||
input string
|
||||
pos int
|
||||
start int
|
||||
width int
|
||||
}
|
||||
|
||||
var (
|
||||
ErrSyntax = errors.New("invalid syntax")
|
||||
dictKeyRex = regexp.MustCompile(`^'([^']*)'$`)
|
||||
sliceOperatorRex = regexp.MustCompile(`^(-?[\d]*)(:-?[\d]*)?(:-?[\d]*)?$`)
|
||||
)
|
||||
|
||||
// Parse parsed the given text and return a node Parser.
|
||||
// If an error is encountered, parsing stops and an empty
|
||||
// Parser is returned with the error
|
||||
func Parse(name, text string) (*Parser, error) {
|
||||
p := NewParser(name)
|
||||
err := p.Parse(text)
|
||||
if err != nil {
|
||||
p = nil
|
||||
}
|
||||
return p, err
|
||||
}
|
||||
|
||||
func NewParser(name string) *Parser {
|
||||
return &Parser{
|
||||
Name: name,
|
||||
}
|
||||
}
|
||||
|
||||
// parseAction parsed the expression inside delimiter
|
||||
func parseAction(name, text string) (*Parser, error) {
|
||||
p, err := Parse(name, fmt.Sprintf("%s%s%s", leftDelim, text, rightDelim))
|
||||
// when error happens, p will be nil, so we need to return here
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.Root = p.Root.Nodes[0].(*ListNode)
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *Parser) Parse(text string) error {
|
||||
p.input = text
|
||||
p.Root = newList()
|
||||
p.pos = 0
|
||||
return p.parseText(p.Root)
|
||||
}
|
||||
|
||||
// consumeText return the parsed text since last cosumeText
|
||||
func (p *Parser) consumeText() string {
|
||||
value := p.input[p.start:p.pos]
|
||||
p.start = p.pos
|
||||
return value
|
||||
}
|
||||
|
||||
// next returns the next rune in the input.
|
||||
func (p *Parser) next() rune {
|
||||
if p.pos >= len(p.input) {
|
||||
p.width = 0
|
||||
return eof
|
||||
}
|
||||
r, w := utf8.DecodeRuneInString(p.input[p.pos:])
|
||||
p.width = w
|
||||
p.pos += p.width
|
||||
return r
|
||||
}
|
||||
|
||||
// peek returns but does not consume the next rune in the input.
|
||||
func (p *Parser) peek() rune {
|
||||
r := p.next()
|
||||
p.backup()
|
||||
return r
|
||||
}
|
||||
|
||||
// backup steps back one rune. Can only be called once per call of next.
|
||||
func (p *Parser) backup() {
|
||||
p.pos -= p.width
|
||||
}
|
||||
|
||||
func (p *Parser) parseText(cur *ListNode) error {
|
||||
for {
|
||||
if strings.HasPrefix(p.input[p.pos:], leftDelim) {
|
||||
if p.pos > p.start {
|
||||
cur.append(newText(p.consumeText()))
|
||||
}
|
||||
return p.parseLeftDelim(cur)
|
||||
}
|
||||
if p.next() == eof {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Correctly reached EOF.
|
||||
if p.pos > p.start {
|
||||
cur.append(newText(p.consumeText()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseLeftDelim scans the left delimiter, which is known to be present.
|
||||
func (p *Parser) parseLeftDelim(cur *ListNode) error {
|
||||
p.pos += len(leftDelim)
|
||||
p.consumeText()
|
||||
newNode := newList()
|
||||
cur.append(newNode)
|
||||
cur = newNode
|
||||
return p.parseInsideAction(cur)
|
||||
}
|
||||
|
||||
func (p *Parser) parseInsideAction(cur *ListNode) error {
|
||||
prefixMap := map[string]func(*ListNode) error{
|
||||
rightDelim: p.parseRightDelim,
|
||||
"[?(": p.parseFilter,
|
||||
"..": p.parseRecursive,
|
||||
}
|
||||
for prefix, parseFunc := range prefixMap {
|
||||
if strings.HasPrefix(p.input[p.pos:], prefix) {
|
||||
return parseFunc(cur)
|
||||
}
|
||||
}
|
||||
|
||||
switch r := p.next(); {
|
||||
case r == eof || isEndOfLine(r):
|
||||
return fmt.Errorf("unclosed action")
|
||||
case r == ' ':
|
||||
p.consumeText()
|
||||
case r == '@' || r == '$': //the current object, just pass it
|
||||
p.consumeText()
|
||||
case r == '[':
|
||||
return p.parseArray(cur)
|
||||
case r == '"' || r == '\'':
|
||||
return p.parseQuote(cur, r)
|
||||
case r == '.':
|
||||
return p.parseField(cur)
|
||||
case r == '+' || r == '-' || unicode.IsDigit(r):
|
||||
p.backup()
|
||||
return p.parseNumber(cur)
|
||||
case isAlphaNumeric(r):
|
||||
p.backup()
|
||||
return p.parseIdentifier(cur)
|
||||
default:
|
||||
return fmt.Errorf("unrecognized character in action: %#U", r)
|
||||
}
|
||||
return p.parseInsideAction(cur)
|
||||
}
|
||||
|
||||
// parseRightDelim scans the right delimiter, which is known to be present.
|
||||
func (p *Parser) parseRightDelim(cur *ListNode) error {
|
||||
p.pos += len(rightDelim)
|
||||
p.consumeText()
|
||||
return p.parseText(p.Root)
|
||||
}
|
||||
|
||||
// parseIdentifier scans build-in keywords, like "range" "end"
|
||||
func (p *Parser) parseIdentifier(cur *ListNode) error {
|
||||
var r rune
|
||||
for {
|
||||
r = p.next()
|
||||
if isTerminator(r) {
|
||||
p.backup()
|
||||
break
|
||||
}
|
||||
}
|
||||
value := p.consumeText()
|
||||
|
||||
if isBool(value) {
|
||||
v, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can not parse bool '%s': %s", value, err.Error())
|
||||
}
|
||||
|
||||
cur.append(newBool(v))
|
||||
} else {
|
||||
cur.append(newIdentifier(value))
|
||||
}
|
||||
|
||||
return p.parseInsideAction(cur)
|
||||
}
|
||||
|
||||
// parseRecursive scans the recursive descent operator ..
|
||||
func (p *Parser) parseRecursive(cur *ListNode) error {
|
||||
if lastIndex := len(cur.Nodes) - 1; lastIndex >= 0 && cur.Nodes[lastIndex].Type() == NodeRecursive {
|
||||
return fmt.Errorf("invalid multiple recursive descent")
|
||||
}
|
||||
p.pos += len("..")
|
||||
p.consumeText()
|
||||
cur.append(newRecursive())
|
||||
if r := p.peek(); isAlphaNumeric(r) {
|
||||
return p.parseField(cur)
|
||||
}
|
||||
return p.parseInsideAction(cur)
|
||||
}
|
||||
|
||||
// parseNumber scans number
|
||||
func (p *Parser) parseNumber(cur *ListNode) error {
|
||||
r := p.peek()
|
||||
if r == '+' || r == '-' {
|
||||
p.next()
|
||||
}
|
||||
for {
|
||||
r = p.next()
|
||||
if r != '.' && !unicode.IsDigit(r) {
|
||||
p.backup()
|
||||
break
|
||||
}
|
||||
}
|
||||
value := p.consumeText()
|
||||
i, err := strconv.Atoi(value)
|
||||
if err == nil {
|
||||
cur.append(newInt(i))
|
||||
return p.parseInsideAction(cur)
|
||||
}
|
||||
d, err := strconv.ParseFloat(value, 64)
|
||||
if err == nil {
|
||||
cur.append(newFloat(d))
|
||||
return p.parseInsideAction(cur)
|
||||
}
|
||||
return fmt.Errorf("cannot parse number %s", value)
|
||||
}
|
||||
|
||||
// parseArray scans array index selection
|
||||
func (p *Parser) parseArray(cur *ListNode) error {
|
||||
Loop:
|
||||
for {
|
||||
switch p.next() {
|
||||
case eof, '\n':
|
||||
return fmt.Errorf("unterminated array")
|
||||
case ']':
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
text := p.consumeText()
|
||||
text = text[1 : len(text)-1]
|
||||
if text == "*" {
|
||||
text = ":"
|
||||
}
|
||||
|
||||
//union operator
|
||||
strs := strings.Split(text, ",")
|
||||
if len(strs) > 1 {
|
||||
union := []*ListNode{}
|
||||
for _, str := range strs {
|
||||
parser, err := parseAction("union", fmt.Sprintf("[%s]", strings.Trim(str, " ")))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
union = append(union, parser.Root)
|
||||
}
|
||||
cur.append(newUnion(union))
|
||||
return p.parseInsideAction(cur)
|
||||
}
|
||||
|
||||
// dict key
|
||||
value := dictKeyRex.FindStringSubmatch(text)
|
||||
if value != nil {
|
||||
parser, err := parseAction("arraydict", fmt.Sprintf(".%s", value[1]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, node := range parser.Root.Nodes {
|
||||
cur.append(node)
|
||||
}
|
||||
return p.parseInsideAction(cur)
|
||||
}
|
||||
|
||||
//slice operator
|
||||
value = sliceOperatorRex.FindStringSubmatch(text)
|
||||
if value == nil {
|
||||
return fmt.Errorf("invalid array index %s", text)
|
||||
}
|
||||
value = value[1:]
|
||||
params := [3]ParamsEntry{}
|
||||
for i := 0; i < 3; i++ {
|
||||
if value[i] != "" {
|
||||
if i > 0 {
|
||||
value[i] = value[i][1:]
|
||||
}
|
||||
if i > 0 && value[i] == "" {
|
||||
params[i].Known = false
|
||||
} else {
|
||||
var err error
|
||||
params[i].Known = true
|
||||
params[i].Value, err = strconv.Atoi(value[i])
|
||||
if err != nil {
|
||||
return fmt.Errorf("array index %s is not a number", value[i])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if i == 1 {
|
||||
params[i].Known = true
|
||||
params[i].Value = params[0].Value + 1
|
||||
params[i].Derived = true
|
||||
} else {
|
||||
params[i].Known = false
|
||||
params[i].Value = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
cur.append(newArray(params))
|
||||
return p.parseInsideAction(cur)
|
||||
}
|
||||
|
||||
// parseFilter scans filter inside array selection
|
||||
func (p *Parser) parseFilter(cur *ListNode) error {
|
||||
p.pos += len("[?(")
|
||||
p.consumeText()
|
||||
begin := false
|
||||
end := false
|
||||
var pair rune
|
||||
|
||||
Loop:
|
||||
for {
|
||||
r := p.next()
|
||||
switch r {
|
||||
case eof, '\n':
|
||||
return fmt.Errorf("unterminated filter")
|
||||
case '"', '\'':
|
||||
if begin == false {
|
||||
//save the paired rune
|
||||
begin = true
|
||||
pair = r
|
||||
continue
|
||||
}
|
||||
//only add when met paired rune
|
||||
if p.input[p.pos-2] != '\\' && r == pair {
|
||||
end = true
|
||||
}
|
||||
case ')':
|
||||
//in rightParser below quotes only appear zero or once
|
||||
//and must be paired at the beginning and end
|
||||
if begin == end {
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
}
|
||||
if p.next() != ']' {
|
||||
return fmt.Errorf("unclosed array expect ]")
|
||||
}
|
||||
reg := regexp.MustCompile(`^([^!<>=]+)([!<>=]+)(.+?)$`)
|
||||
text := p.consumeText()
|
||||
text = text[:len(text)-2]
|
||||
value := reg.FindStringSubmatch(text)
|
||||
if value == nil {
|
||||
parser, err := parseAction("text", text)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cur.append(newFilter(parser.Root, newList(), "exists"))
|
||||
} else {
|
||||
leftParser, err := parseAction("left", value[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rightParser, err := parseAction("right", value[3])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cur.append(newFilter(leftParser.Root, rightParser.Root, value[2]))
|
||||
}
|
||||
return p.parseInsideAction(cur)
|
||||
}
|
||||
|
||||
// parseQuote unquotes string inside double or single quote
|
||||
func (p *Parser) parseQuote(cur *ListNode, end rune) error {
|
||||
Loop:
|
||||
for {
|
||||
switch p.next() {
|
||||
case eof, '\n':
|
||||
return fmt.Errorf("unterminated quoted string")
|
||||
case end:
|
||||
//if it's not escape break the Loop
|
||||
if p.input[p.pos-2] != '\\' {
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
}
|
||||
value := p.consumeText()
|
||||
s, err := UnquoteExtend(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unquote string %s error %v", value, err)
|
||||
}
|
||||
cur.append(newText(s))
|
||||
return p.parseInsideAction(cur)
|
||||
}
|
||||
|
||||
// parseField scans a field until a terminator
|
||||
func (p *Parser) parseField(cur *ListNode) error {
|
||||
p.consumeText()
|
||||
for p.advance() {
|
||||
}
|
||||
value := p.consumeText()
|
||||
if value == "*" {
|
||||
cur.append(newWildcard())
|
||||
} else {
|
||||
cur.append(newField(strings.Replace(value, "\\", "", -1)))
|
||||
}
|
||||
return p.parseInsideAction(cur)
|
||||
}
|
||||
|
||||
// advance scans until next non-escaped terminator
|
||||
func (p *Parser) advance() bool {
|
||||
r := p.next()
|
||||
if r == '\\' {
|
||||
p.next()
|
||||
} else if isTerminator(r) {
|
||||
p.backup()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// isTerminator reports whether the input is at valid termination character to appear after an identifier.
|
||||
func isTerminator(r rune) bool {
|
||||
if isSpace(r) || isEndOfLine(r) {
|
||||
return true
|
||||
}
|
||||
switch r {
|
||||
case eof, '.', ',', '[', ']', '$', '@', '{', '}':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isSpace reports whether r is a space character.
|
||||
func isSpace(r rune) bool {
|
||||
return r == ' ' || r == '\t'
|
||||
}
|
||||
|
||||
// isEndOfLine reports whether r is an end-of-line character.
|
||||
func isEndOfLine(r rune) bool {
|
||||
return r == '\r' || r == '\n'
|
||||
}
|
||||
|
||||
// isAlphaNumeric reports whether r is an alphabetic, digit, or underscore.
|
||||
func isAlphaNumeric(r rune) bool {
|
||||
return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)
|
||||
}
|
||||
|
||||
// isBool reports whether s is a boolean value.
|
||||
func isBool(s string) bool {
|
||||
return s == "true" || s == "false"
|
||||
}
|
||||
|
||||
// UnquoteExtend is almost same as strconv.Unquote(), but it support parse single quotes as a string
|
||||
func UnquoteExtend(s string) (string, error) {
|
||||
n := len(s)
|
||||
if n < 2 {
|
||||
return "", ErrSyntax
|
||||
}
|
||||
quote := s[0]
|
||||
if quote != s[n-1] {
|
||||
return "", ErrSyntax
|
||||
}
|
||||
s = s[1 : n-1]
|
||||
|
||||
if quote != '"' && quote != '\'' {
|
||||
return "", ErrSyntax
|
||||
}
|
||||
|
||||
// Is it trivial? Avoid allocation.
|
||||
if !contains(s, '\\') && !contains(s, quote) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
var runeTmp [utf8.UTFMax]byte
|
||||
buf := make([]byte, 0, 3*len(s)/2) // Try to avoid more allocations.
|
||||
for len(s) > 0 {
|
||||
c, multibyte, ss, err := strconv.UnquoteChar(s, quote)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
s = ss
|
||||
if c < utf8.RuneSelf || !multibyte {
|
||||
buf = append(buf, byte(c))
|
||||
} else {
|
||||
n := utf8.EncodeRune(runeTmp[:], c)
|
||||
buf = append(buf, runeTmp[:n]...)
|
||||
}
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
func contains(s string, c byte) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == c {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
reviewers:
|
||||
- caesarxuchao
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
Copyright 2016 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package retry
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
)
|
||||
|
||||
// DefaultRetry is the recommended retry for a conflict where multiple clients
|
||||
// are making changes to the same resource.
|
||||
var DefaultRetry = wait.Backoff{
|
||||
Steps: 5,
|
||||
Duration: 10 * time.Millisecond,
|
||||
Factor: 1.0,
|
||||
Jitter: 0.1,
|
||||
}
|
||||
|
||||
// DefaultBackoff is the recommended backoff for a conflict where a client
|
||||
// may be attempting to make an unrelated modification to a resource under
|
||||
// active management by one or more controllers.
|
||||
var DefaultBackoff = wait.Backoff{
|
||||
Steps: 4,
|
||||
Duration: 10 * time.Millisecond,
|
||||
Factor: 5.0,
|
||||
Jitter: 0.1,
|
||||
}
|
||||
|
||||
// OnError allows the caller to retry fn in case the error returned by fn is retriable
|
||||
// according to the provided function. backoff defines the maximum retries and the wait
|
||||
// interval between two retries.
|
||||
func OnError(backoff wait.Backoff, retriable func(error) bool, fn func() error) error {
|
||||
var lastErr error
|
||||
err := wait.ExponentialBackoff(backoff, func() (bool, error) {
|
||||
err := fn()
|
||||
switch {
|
||||
case err == nil:
|
||||
return true, nil
|
||||
case retriable(err):
|
||||
lastErr = err
|
||||
return false, nil
|
||||
default:
|
||||
return false, err
|
||||
}
|
||||
})
|
||||
if wait.Interrupted(err) {
|
||||
err = lastErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// RetryOnConflict is used to make an update to a resource when you have to worry about
|
||||
// conflicts caused by other code making unrelated updates to the resource at the same
|
||||
// time. fn should fetch the resource to be modified, make appropriate changes to it, try
|
||||
// to update it, and return (unmodified) the error from the update function. On a
|
||||
// successful update, RetryOnConflict will return nil. If the update function returns a
|
||||
// "Conflict" error, RetryOnConflict will wait some amount of time as described by
|
||||
// backoff, and then try again. On a non-"Conflict" error, or if it retries too many times
|
||||
// and gives up, RetryOnConflict will return an error to the caller.
|
||||
//
|
||||
// err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
// // Fetch the resource here; you need to refetch it on every try, since
|
||||
// // if you got a conflict on the last update attempt then you need to get
|
||||
// // the current version before making your own changes.
|
||||
// pod, err := c.Pods("mynamespace").Get(name, metav1.GetOptions{})
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
//
|
||||
// // Make whatever updates to the resource are needed
|
||||
// pod.Status.Phase = v1.PodFailed
|
||||
//
|
||||
// // Try to update
|
||||
// _, err = c.Pods("mynamespace").UpdateStatus(pod)
|
||||
// // You have to return err itself here (not wrapped inside another error)
|
||||
// // so that RetryOnConflict can identify it correctly.
|
||||
// return err
|
||||
// })
|
||||
// if err != nil {
|
||||
// // May be conflict if max retries were hit, or may be something unrelated
|
||||
// // like permissions or a network error
|
||||
// return err
|
||||
// }
|
||||
// ...
|
||||
//
|
||||
// TODO: Make Backoff an interface?
|
||||
func RetryOnConflict(backoff wait.Backoff, fn func() error) error {
|
||||
return OnError(backoff, errors.IsConflict, fn)
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
Copyright 2024 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package watchlist
|
||||
|
||||
import (
|
||||
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
metainternalversionvalidation "k8s.io/apimachinery/pkg/apis/meta/internalversion/validation"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientfeatures "k8s.io/client-go/features"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
var scheme = runtime.NewScheme()
|
||||
|
||||
func init() {
|
||||
utilruntime.Must(metainternalversion.AddToScheme(scheme))
|
||||
}
|
||||
|
||||
// PrepareWatchListOptionsFromListOptions creates a new ListOptions
|
||||
// that can be used for a watch-list request from the given listOptions.
|
||||
//
|
||||
// This function also determines if the given listOptions can be used to form a watch-list request,
|
||||
// which would result in streaming semantically equivalent data from the server.
|
||||
func PrepareWatchListOptionsFromListOptions(listOptions metav1.ListOptions) (metav1.ListOptions, bool, error) {
|
||||
if !clientfeatures.FeatureGates().Enabled(clientfeatures.WatchListClient) {
|
||||
return metav1.ListOptions{}, false, nil
|
||||
}
|
||||
|
||||
internalListOptions := &metainternalversion.ListOptions{}
|
||||
if err := scheme.Convert(&listOptions, internalListOptions, nil); err != nil {
|
||||
return metav1.ListOptions{}, false, err
|
||||
}
|
||||
if errs := metainternalversionvalidation.ValidateListOptions(internalListOptions, true); len(errs) > 0 {
|
||||
return metav1.ListOptions{}, false, nil
|
||||
}
|
||||
|
||||
watchListOptions := listOptions
|
||||
// this is our legacy case, the cache ignores LIMIT for
|
||||
// ResourceVersion == 0 and RVM=unset|NotOlderThan
|
||||
if listOptions.Limit > 0 && listOptions.ResourceVersion != "0" {
|
||||
return metav1.ListOptions{}, false, nil
|
||||
}
|
||||
watchListOptions.Limit = 0
|
||||
|
||||
// to ensure that we can create a watch-list request that returns
|
||||
// semantically equivalent data for the given listOptions,
|
||||
// we need to validate that the RVM for the list is supported by watch-list requests.
|
||||
if listOptions.ResourceVersionMatch == metav1.ResourceVersionMatchExact {
|
||||
return metav1.ListOptions{}, false, nil
|
||||
}
|
||||
watchListOptions.ResourceVersionMatch = metav1.ResourceVersionMatchNotOlderThan
|
||||
|
||||
watchListOptions.Watch = true
|
||||
watchListOptions.AllowWatchBookmarks = true
|
||||
watchListOptions.SendInitialEvents = ptr.To(true)
|
||||
|
||||
internalWatchListOptions := &metainternalversion.ListOptions{}
|
||||
if err := scheme.Convert(&watchListOptions, internalWatchListOptions, nil); err != nil {
|
||||
return metav1.ListOptions{}, false, err
|
||||
}
|
||||
if errs := metainternalversionvalidation.ValidateListOptions(internalWatchListOptions, true); len(errs) > 0 {
|
||||
return metav1.ListOptions{}, false, nil
|
||||
}
|
||||
|
||||
return watchListOptions, true, nil
|
||||
}
|
||||
|
||||
type unSupportedWatchListSemantics interface {
|
||||
IsWatchListSemanticsUnSupported() bool
|
||||
}
|
||||
|
||||
// DoesClientNotSupportWatchListSemantics reports whether the given client
|
||||
// does NOT support WatchList semantics.
|
||||
//
|
||||
// A client does NOT support WatchList only if
|
||||
// it implements `IsWatchListSemanticsUnSupported` and that returns true.
|
||||
func DoesClientNotSupportWatchListSemantics(client any) bool {
|
||||
lw, ok := client.(unSupportedWatchListSemantics)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return lw.IsWatchListSemanticsUnSupported()
|
||||
}
|
||||
Reference in New Issue
Block a user