diff --git a/pkg/storage/internalstorage/features.go b/pkg/storage/internalstorage/features.go new file mode 100644 index 0000000000000000000000000000000000000000..b59ec093d0c8575b2e381be883984d4b21f04a50 --- /dev/null +++ b/pkg/storage/internalstorage/features.go @@ -0,0 +1,25 @@ +package internalstorage + +import ( + "k8s.io/apimachinery/pkg/util/runtime" + utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/component-base/featuregate" +) + +const ( + // AllowRawSQLQuery is a feature gate for the apiserver to allow querying by the raw sql. + // + // owner: @cleverhu + // alpha: v0.3.0 + AllowRawSQLQuery featuregate.Feature = "AllowRawSQLQuery" +) + +func init() { + runtime.Must(utilfeature.DefaultMutableFeatureGate.Add(defaultInternalStorageFeatureGates)) +} + +// defaultInternalStorageFeatureGates consists of all known custom internalstorage feature keys. +// To add a new feature, define a key for it above and add it here. +var defaultInternalStorageFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ + AllowRawSQLQuery: {Default: false, PreRelease: featuregate.Alpha}, +} diff --git a/pkg/storage/internalstorage/util.go b/pkg/storage/internalstorage/util.go index bef36ad9f8c5197f41593ea2b687770cea9a2c97..9d9faf2d105a1ddde5ac28ad2f9055a065970b67 100644 --- a/pkg/storage/internalstorage/util.go +++ b/pkg/storage/internalstorage/util.go @@ -14,10 +14,15 @@ import ( "k8s.io/apimachinery/pkg/selection" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation/field" + utilfeature "k8s.io/apiserver/pkg/util/feature" internal "github.com/clusterpedia-io/api/clusterpedia" ) +const ( + URLQueryWhereSQL = "whereSQL" +) + var ( supportedOrderByFields = sets.NewString("cluster", "namespace", "name", "created_at", "resource_version") ) @@ -55,6 +60,15 @@ func applyListOptionsToQuery(query *gorm.DB, opts *internal.ListOptions, applyFn query = query.Where("created_at < ?", opts.Before.Time.UTC()) } + if utilfeature.DefaultMutableFeatureGate.Enabled(AllowRawSQLQuery) { + if len(opts.URLQuery[URLQueryWhereSQL]) > 0 { + // TODO: prevent SQL injection. + // If a string of numbers is passed in from SQL, the query will be taken as ID by default. + // If the SQL contains English letter, it will be passed in as column. + query = query.Where(opts.URLQuery[URLQueryWhereSQL][0]) + } + } + if opts.LabelSelector != nil { if requirements, selectable := opts.LabelSelector.Requirements(); selectable { for _, requirement := range requirements { @@ -137,7 +151,7 @@ func applyListOptionsToQuery(query *gorm.DB, opts *internal.ListOptions, applyFn for _, orderby := range opts.OrderBy { field := orderby.Field if supportedOrderByFields.Has(field) { - if field == "resource_version"{ + if field == "resource_version" { field = "CAST(resource_version as decimal)" } column := clause.OrderByColumn{ diff --git a/staging/src/github.com/clusterpedia-io/api/clusterpedia/types.go b/staging/src/github.com/clusterpedia-io/api/clusterpedia/types.go index d4f7b059e7c2128f11c15cec5731a029810509d1..e6a8faa779c6f1e5d4e272b4ce214f65d65f045e 100644 --- a/staging/src/github.com/clusterpedia-io/api/clusterpedia/types.go +++ b/staging/src/github.com/clusterpedia-io/api/clusterpedia/types.go @@ -68,7 +68,7 @@ type ListOptions struct { ExtraLabelSelector labels.Selector // +k8s:conversion-fn:drop - ExtraQuery url.Values + URLQuery url.Values // RelatedResources []schema.GroupVersionKind } diff --git a/staging/src/github.com/clusterpedia-io/api/clusterpedia/v1beta1/conversion.go b/staging/src/github.com/clusterpedia-io/api/clusterpedia/v1beta1/conversion.go index 59b6a58a6a6e8f180772b590f99d05968de5e077..460fc62ac994b16e5e018d1edfc3bbe88d5b3d49 100644 --- a/staging/src/github.com/clusterpedia-io/api/clusterpedia/v1beta1/conversion.go +++ b/staging/src/github.com/clusterpedia-io/api/clusterpedia/v1beta1/conversion.go @@ -183,6 +183,10 @@ func Convert_v1beta1_ListOptions_To_clusterpedia_ListOptions(in *ListOptions, ou if out.Before.Before(out.Since) { return fmt.Errorf("Invalid Query, Since is after Before") } + if len(in.urlQuery) > 0 { + // Out URLQuery will not be modified, so deepcopy is not used here. + out.URLQuery = in.urlQuery + } return nil } @@ -229,6 +233,8 @@ func Convert_url_Values_To_v1beta1_ListOptions(in *url.Values, out *ListOptions, if err := metav1.Convert_url_Values_To_v1_ListOptions(in, &out.ListOptions, s); err != nil { return err } + // Save the native query parameters for use by listoptions. + out.urlQuery = *in return autoConvert_url_Values_To_v1beta1_ListOptions(in, out, s) } diff --git a/staging/src/github.com/clusterpedia-io/api/clusterpedia/v1beta1/types.go b/staging/src/github.com/clusterpedia-io/api/clusterpedia/v1beta1/types.go index 6c667e0a42d68e34d0c108ed6b06298437712ad7..28b5e5ff03cca3644fb5aebf9b5041559d245f67 100644 --- a/staging/src/github.com/clusterpedia-io/api/clusterpedia/v1beta1/types.go +++ b/staging/src/github.com/clusterpedia-io/api/clusterpedia/v1beta1/types.go @@ -1,6 +1,8 @@ package v1beta1 import ( + "net/url" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -46,6 +48,8 @@ type ListOptions struct { // +optional WithRemainingCount *bool `json:"withRemainingCount,omitempty"` + + urlQuery url.Values } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/staging/src/github.com/clusterpedia-io/api/clusterpedia/v1beta1/zz_generated.conversion.go b/staging/src/github.com/clusterpedia-io/api/clusterpedia/v1beta1/zz_generated.conversion.go index 02851d6a978ad5fe3eaabed0e58f4f6869e2d4bb..a306b897cb555b2df974592bcc6dccde785bf10e 100644 --- a/staging/src/github.com/clusterpedia-io/api/clusterpedia/v1beta1/zz_generated.conversion.go +++ b/staging/src/github.com/clusterpedia-io/api/clusterpedia/v1beta1/zz_generated.conversion.go @@ -201,6 +201,7 @@ func autoConvert_v1beta1_ListOptions_To_clusterpedia_ListOptions(in *ListOptions out.OwnerSeniority = in.OwnerSeniority out.WithContinue = (*bool)(unsafe.Pointer(in.WithContinue)) out.WithRemainingCount = (*bool)(unsafe.Pointer(in.WithRemainingCount)) + // WARNING: in.urlQuery requires manual conversion: does not exist in peer-type return nil } @@ -227,7 +228,7 @@ func autoConvert_clusterpedia_ListOptions_To_v1beta1_ListOptions(in *clusterpedi out.WithRemainingCount = (*bool)(unsafe.Pointer(in.WithRemainingCount)) // WARNING: in.EnhancedFieldSelector requires manual conversion: does not exist in peer-type // WARNING: in.ExtraLabelSelector requires manual conversion: does not exist in peer-type - // WARNING: in.ExtraQuery requires manual conversion: does not exist in peer-type + // WARNING: in.URLQuery requires manual conversion: does not exist in peer-type return nil } @@ -318,5 +319,7 @@ func autoConvert_url_Values_To_v1beta1_ListOptions(in *url.Values, out *ListOpti } else { out.WithRemainingCount = nil } + // WARNING: Field urlQuery does not have json tag, skipping. + return nil } diff --git a/staging/src/github.com/clusterpedia-io/api/clusterpedia/v1beta1/zz_generated.deepcopy.go b/staging/src/github.com/clusterpedia-io/api/clusterpedia/v1beta1/zz_generated.deepcopy.go index 100ba415c72999891b6c6a568c48c2bb9c70ae05..ae0ca2e62bfe77e0a4352d997c54f223b3036dd2 100644 --- a/staging/src/github.com/clusterpedia-io/api/clusterpedia/v1beta1/zz_generated.deepcopy.go +++ b/staging/src/github.com/clusterpedia-io/api/clusterpedia/v1beta1/zz_generated.deepcopy.go @@ -6,6 +6,8 @@ package v1beta1 import ( + url "net/url" + runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -110,6 +112,21 @@ func (in *ListOptions) DeepCopyInto(out *ListOptions) { *out = new(bool) **out = **in } + if in.urlQuery != nil { + in, out := &in.urlQuery, &out.urlQuery + *out = make(url.Values, len(*in)) + for key, val := range *in { + var outVal []string + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = make([]string, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } return } diff --git a/staging/src/github.com/clusterpedia-io/api/clusterpedia/zz_generated.deepcopy.go b/staging/src/github.com/clusterpedia-io/api/clusterpedia/zz_generated.deepcopy.go index f44a9a1e7ce21b5eea35ebabadbce724789b10a5..cceef58d923527d9a7c5905216eb4e83afb9b669 100644 --- a/staging/src/github.com/clusterpedia-io/api/clusterpedia/zz_generated.deepcopy.go +++ b/staging/src/github.com/clusterpedia-io/api/clusterpedia/zz_generated.deepcopy.go @@ -149,8 +149,8 @@ func (in *ListOptions) DeepCopyInto(out *ListOptions) { if in.ExtraLabelSelector != nil { out.ExtraLabelSelector = in.ExtraLabelSelector.DeepCopySelector() } - if in.ExtraQuery != nil { - in, out := &in.ExtraQuery, &out.ExtraQuery + if in.URLQuery != nil { + in, out := &in.URLQuery, &out.URLQuery *out = make(url.Values, len(*in)) for key, val := range *in { var outVal []string