mirror of
https://github.com/kubevela/kubevela.git
synced 2026-02-14 18:10:21 +00:00
Feat: initial provider generator (#5839)
* Feat: initial provider generator Signed-off-by: iyear <ljyngup@gmail.com> * Fix: distinguish any and ellipsis type Signed-off-by: iyear <ljyngup@gmail.com> --------- Signed-off-by: iyear <ljyngup@gmail.com>
This commit is contained in:
198
references/cuegen/generators/provider/provider.go
Normal file
198
references/cuegen/generators/provider/provider.go
Normal file
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
Copyright 2023 The KubeVela 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 provider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
goast "go/ast"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
cueast "cuelang.org/go/cue/ast"
|
||||
cuetoken "cuelang.org/go/cue/token"
|
||||
"golang.org/x/tools/go/packages"
|
||||
|
||||
"github.com/oam-dev/kubevela/references/cuegen"
|
||||
)
|
||||
|
||||
const (
|
||||
typeProviderFnMap = "map[string]github.com/kubevela/pkg/cue/cuex/runtime.ProviderFn"
|
||||
typeProvidersParamsPrefix = "github.com/kubevela/pkg/cue/cuex/providers.Params"
|
||||
typeProvidersReturnsPrefix = "github.com/kubevela/pkg/cue/cuex/providers.Returns"
|
||||
)
|
||||
|
||||
const (
|
||||
doKey = "do"
|
||||
providerKey = "provider"
|
||||
)
|
||||
|
||||
type provider struct {
|
||||
name string
|
||||
params string
|
||||
returns string
|
||||
do string
|
||||
}
|
||||
|
||||
// Options is options of generation
|
||||
type Options struct {
|
||||
File string // Go file path
|
||||
Writer io.Writer // target writer
|
||||
Types map[string]cuegen.Type // option cuegen.WithTypes
|
||||
Nullable bool // option cuegen.WithNullable
|
||||
}
|
||||
|
||||
// Generate generates cue provider from Go struct
|
||||
func Generate(opts Options) (rerr error) {
|
||||
g, err := cuegen.NewGenerator(opts.File)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// make options
|
||||
genOpts := make([]cuegen.Option, 0)
|
||||
// any types
|
||||
genOpts = append(genOpts, cuegen.WithTypes(opts.Types))
|
||||
// nullable
|
||||
if opts.Nullable {
|
||||
genOpts = append(genOpts, cuegen.WithNullable())
|
||||
}
|
||||
// type filter
|
||||
genOpts = append(genOpts, cuegen.WithTypeFilter(func(spec *goast.TypeSpec) bool {
|
||||
typ := g.Package().TypesInfo.TypeOf(spec.Type)
|
||||
// only process provider params and returns.
|
||||
if strings.HasPrefix(typ.String(), typeProvidersParamsPrefix) ||
|
||||
strings.HasPrefix(typ.String(), typeProvidersReturnsPrefix) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}))
|
||||
|
||||
decls, err := g.Generate(genOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
providers, err := extractProviders(g.Package())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newDecls, err := modifyDecls(g.Package().Name, decls, providers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return g.Format(opts.Writer, newDecls)
|
||||
}
|
||||
|
||||
// extractProviders extracts the providers from map[string]cuexruntime.ProviderFn
|
||||
func extractProviders(pkg *packages.Package) (providers []provider, rerr error) {
|
||||
var (
|
||||
providersMap *goast.CompositeLit
|
||||
ok bool
|
||||
)
|
||||
// extract provider def map
|
||||
for k, v := range pkg.TypesInfo.Types {
|
||||
if v.Type.String() != typeProviderFnMap {
|
||||
continue
|
||||
}
|
||||
|
||||
if providersMap, ok = k.(*goast.CompositeLit); ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if providersMap == nil {
|
||||
return nil, fmt.Errorf("no provider function map found like '%s'", typeProviderFnMap)
|
||||
}
|
||||
|
||||
defer recoverAssert(&rerr, "extract providers")
|
||||
|
||||
for _, e := range providersMap.Elts {
|
||||
pair := e.(*goast.KeyValueExpr)
|
||||
doName := pair.Key.(*goast.BasicLit)
|
||||
value := pair.Value.(*goast.CallExpr)
|
||||
|
||||
indices := value.Fun.(*goast.IndexListExpr)
|
||||
params := indices.Indices[0].(*goast.Ident) // params struct name
|
||||
returns := indices.Indices[1].(*goast.Ident) // returns struct name
|
||||
|
||||
do := value.Args[0].(*goast.Ident)
|
||||
|
||||
providers = append(providers, provider{
|
||||
name: doName.Value,
|
||||
params: params.Name,
|
||||
returns: returns.Name,
|
||||
do: do.Name,
|
||||
})
|
||||
}
|
||||
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
// modifyDecls re-generates cue ast decls of providers.
|
||||
func modifyDecls(provider string, old []cueast.Decl, providers []provider) (decls []cueast.Decl, rerr error) {
|
||||
defer recoverAssert(&rerr, "modify decls failed")
|
||||
|
||||
// map[StructName]StructLit
|
||||
mapping := make(map[string]cueast.Expr)
|
||||
for _, decl := range old {
|
||||
field := decl.(*cueast.Field)
|
||||
key := field.Label.(*cueast.Ident)
|
||||
|
||||
mapping[key.Name] = field.Value
|
||||
}
|
||||
|
||||
providerField := &cueast.Field{
|
||||
Label: cuegen.Ident(providerKey, true),
|
||||
Value: cueast.NewString(provider),
|
||||
}
|
||||
|
||||
for _, p := range providers {
|
||||
params := mapping[p.params].(*cueast.StructLit).Elts
|
||||
returns := mapping[p.returns].(*cueast.StructLit).Elts
|
||||
|
||||
doField := &cueast.Field{
|
||||
Label: cuegen.Ident(doKey, true),
|
||||
Value: cueast.NewLit(cuetoken.STRING, p.name), // p.name has contained double quotes
|
||||
}
|
||||
|
||||
pdecls := []cueast.Decl{doField, providerField}
|
||||
pdecls = append(pdecls, params...)
|
||||
pdecls = append(pdecls, returns...)
|
||||
|
||||
newProvider := &cueast.Field{
|
||||
Label: cuegen.Ident(p.do, true),
|
||||
Value: &cueast.StructLit{
|
||||
Elts: pdecls,
|
||||
},
|
||||
}
|
||||
cueast.SetRelPos(newProvider, cuetoken.NewSection)
|
||||
|
||||
decls = append(decls, newProvider)
|
||||
}
|
||||
|
||||
return decls, nil
|
||||
}
|
||||
|
||||
// recoverAssert captures panic caused by invalid type assertion or out of range index,
|
||||
// so we don't need to check each type assertion and index
|
||||
func recoverAssert(err *error, msg string) {
|
||||
if r := recover(); r != nil {
|
||||
*err = fmt.Errorf("%s: panic: %v", r, msg)
|
||||
}
|
||||
}
|
||||
80
references/cuegen/generators/provider/provider_test.go
Normal file
80
references/cuegen/generators/provider/provider_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
Copyright 2023 The KubeVela 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 provider
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/oam-dev/kubevela/references/cuegen"
|
||||
)
|
||||
|
||||
func TestGenerate(t *testing.T) {
|
||||
got := bytes.Buffer{}
|
||||
err := Generate(Options{
|
||||
File: "testdata/valid.go",
|
||||
Writer: &got,
|
||||
Types: map[string]cuegen.Type{
|
||||
"*k8s.io/apimachinery/pkg/apis/meta/v1/unstructured.Unstructured": cuegen.TypeEllipsis,
|
||||
"*k8s.io/apimachinery/pkg/apis/meta/v1/unstructured.UnstructuredList": cuegen.TypeEllipsis,
|
||||
},
|
||||
Nullable: false,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
expected, err := os.ReadFile("testdata/valid.cue")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, string(expected), got.String())
|
||||
}
|
||||
|
||||
func TestGenerateInvalid(t *testing.T) {
|
||||
if err := filepath.Walk("testdata/invalid", func(path string, info os.FileInfo, e error) error {
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := Generate(Options{
|
||||
File: path,
|
||||
Writer: io.Discard,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateEmptyError(t *testing.T) {
|
||||
err := Generate(Options{
|
||||
File: "",
|
||||
Writer: io.Discard,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
36
references/cuegen/generators/provider/testdata/invalid/no_provider_map.go
vendored
Normal file
36
references/cuegen/generators/provider/testdata/invalid/no_provider_map.go
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
Copyright 2023 The KubeVela 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 invalid
|
||||
|
||||
import (
|
||||
"github.com/kubevela/pkg/cue/cuex/providers"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
// ResourceVars .
|
||||
type ResourceVars struct {
|
||||
Field1 string `json:"field1"`
|
||||
Field2 *unstructured.Unstructured `json:"field2"`
|
||||
}
|
||||
|
||||
// ResourceParams is the params for resource
|
||||
type ResourceParams providers.Params[ResourceVars]
|
||||
|
||||
// ResourceReturns is the returns for resource
|
||||
type ResourceReturns providers.Returns[*unstructured.Unstructured]
|
||||
|
||||
// No provider map provided
|
||||
98
references/cuegen/generators/provider/testdata/valid.cue
vendored
Normal file
98
references/cuegen/generators/provider/testdata/valid.cue
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
package test
|
||||
|
||||
#Apply: {
|
||||
#do: "apply"
|
||||
#provider: "test"
|
||||
$params: {
|
||||
// +usage=The cluster to use
|
||||
cluster: string
|
||||
// +usage=The resource to get or apply
|
||||
resource: {
|
||||
...
|
||||
}
|
||||
// +usage=The options to get or apply
|
||||
options: {
|
||||
// +usage=The strategy of the resource
|
||||
threeWayMergePatch: {
|
||||
// +usage=The strategy to get or apply the resource
|
||||
enabled: *true | bool
|
||||
// +usage=The annotation prefix to use for the three way merge patch
|
||||
annotationPrefix: *"resource" | string
|
||||
}
|
||||
}
|
||||
}
|
||||
$returns: {
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
#Get: {
|
||||
#do: "get"
|
||||
#provider: "test"
|
||||
$params: {
|
||||
// +usage=The cluster to use
|
||||
cluster: string
|
||||
// +usage=The resource to get or apply
|
||||
resource: {
|
||||
...
|
||||
}
|
||||
// +usage=The options to get or apply
|
||||
options: {
|
||||
// +usage=The strategy of the resource
|
||||
threeWayMergePatch: {
|
||||
// +usage=The strategy to get or apply the resource
|
||||
enabled: *true | bool
|
||||
// +usage=The annotation prefix to use for the three way merge patch
|
||||
annotationPrefix: *"resource" | string
|
||||
}
|
||||
}
|
||||
}
|
||||
$returns: {
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
#List: {
|
||||
#do: "list"
|
||||
#provider: "test"
|
||||
$params: {
|
||||
// +usage=The cluster to use
|
||||
cluster: string
|
||||
// +usage=The filter to list the resources
|
||||
filter?: {
|
||||
// +usage=The namespace to list the resources
|
||||
namespace?: string
|
||||
// +usage=The label selector to filter the resources
|
||||
matchingLabels?: [string]: string
|
||||
}
|
||||
// +usage=The resource to list
|
||||
resource: {
|
||||
...
|
||||
}
|
||||
}
|
||||
$returns: {
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
#Patch: {
|
||||
#do: "patch"
|
||||
#provider: "test"
|
||||
$params: {
|
||||
// +usage=The cluster to use
|
||||
cluster: string
|
||||
// +usage=The resource to patch
|
||||
resource: {
|
||||
...
|
||||
}
|
||||
// +usage=The patch to be applied to the resource with kubernetes patch
|
||||
patch: {
|
||||
// +usage=The type of patch being provided
|
||||
type: "merge" | "json" | "strategic"
|
||||
data: _
|
||||
}
|
||||
}
|
||||
$returns: {
|
||||
...
|
||||
}
|
||||
}
|
||||
133
references/cuegen/generators/provider/testdata/valid.go
vendored
Normal file
133
references/cuegen/generators/provider/testdata/valid.go
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
Copyright 2023 The KubeVela 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 test copied and modified from https://github.com/kubevela/pkg/blob/main/cue/cuex/providers/kube/kube.go.
|
||||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
|
||||
"github.com/kubevela/pkg/cue/cuex/providers"
|
||||
cuexruntime "github.com/kubevela/pkg/cue/cuex/runtime"
|
||||
"github.com/kubevela/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
// ResourceVars .
|
||||
type ResourceVars struct {
|
||||
// +usage=The cluster to use
|
||||
Cluster string `json:"cluster"`
|
||||
// +usage=The resource to get or apply
|
||||
Resource *unstructured.Unstructured `json:"resource"`
|
||||
// +usage=The options to get or apply
|
||||
Options ApplyOptions `json:"options"`
|
||||
}
|
||||
|
||||
// ApplyOptions .
|
||||
type ApplyOptions struct {
|
||||
// +usage=The strategy of the resource
|
||||
ThreeWayMergePatch ThreeWayMergePatchOptions `json:"threeWayMergePatch"`
|
||||
}
|
||||
|
||||
// ThreeWayMergePatchOptions .
|
||||
type ThreeWayMergePatchOptions struct {
|
||||
// +usage=The strategy to get or apply the resource
|
||||
Enabled bool `json:"enabled" cue:"default:true"`
|
||||
// +usage=The annotation prefix to use for the three way merge patch
|
||||
AnnotationPrefix string `json:"annotationPrefix" cue:"default:resource"`
|
||||
}
|
||||
|
||||
// ResourceParams is the params for resource
|
||||
type ResourceParams providers.Params[ResourceVars]
|
||||
|
||||
// ResourceReturns is the returns for resource
|
||||
type ResourceReturns providers.Returns[*unstructured.Unstructured]
|
||||
|
||||
// Apply .
|
||||
func Apply(_ context.Context, _ *ResourceParams) (*ResourceReturns, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Get .
|
||||
func Get(_ context.Context, _ *ResourceParams) (*ResourceReturns, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// ListFilter filter for list resources
|
||||
type ListFilter struct {
|
||||
// +usage=The namespace to list the resources
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
// +usage=The label selector to filter the resources
|
||||
MatchingLabels map[string]string `json:"matchingLabels,omitempty"`
|
||||
}
|
||||
|
||||
// ListVars is the vars for list
|
||||
type ListVars struct {
|
||||
// +usage=The cluster to use
|
||||
Cluster string `json:"cluster"`
|
||||
// +usage=The filter to list the resources
|
||||
Filter *ListFilter `json:"filter,omitempty"`
|
||||
// +usage=The resource to list
|
||||
Resource *unstructured.Unstructured `json:"resource"`
|
||||
}
|
||||
|
||||
// ListParams is the params for list
|
||||
type ListParams providers.Params[ListVars]
|
||||
|
||||
// ListReturns is the returns for list
|
||||
type ListReturns providers.Returns[*unstructured.UnstructuredList]
|
||||
|
||||
// List .
|
||||
func List(_ context.Context, _ *ListParams) (*ListReturns, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// PatchVars is the vars for patch
|
||||
type PatchVars struct {
|
||||
// +usage=The cluster to use
|
||||
Cluster string `json:"cluster"`
|
||||
// +usage=The resource to patch
|
||||
Resource *unstructured.Unstructured `json:"resource"`
|
||||
// +usage=The patch to be applied to the resource with kubernetes patch
|
||||
Patch Patcher `json:"patch"`
|
||||
}
|
||||
|
||||
// Patcher is the patcher
|
||||
type Patcher struct {
|
||||
// +usage=The type of patch being provided
|
||||
Type string `json:"type" cue:"enum:merge,json,strategic;default:merge"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
// PatchParams is the params for patch
|
||||
type PatchParams providers.Params[PatchVars]
|
||||
|
||||
// Patch patches a kubernetes resource with patch strategy
|
||||
func Patch(_ context.Context, _ *PatchParams) (*ResourceReturns, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// ProviderName .
|
||||
const ProviderName = "kube"
|
||||
|
||||
// Package .
|
||||
var Package = runtime.Must(cuexruntime.NewInternalPackage(ProviderName, "", map[string]cuexruntime.ProviderFn{
|
||||
"apply": cuexruntime.GenericProviderFn[ResourceParams, ResourceReturns](Apply),
|
||||
"get": cuexruntime.GenericProviderFn[ResourceParams, ResourceReturns](Get),
|
||||
"list": cuexruntime.GenericProviderFn[ListParams, ListReturns](List),
|
||||
"patch": cuexruntime.GenericProviderFn[PatchParams, ResourceReturns](Patch),
|
||||
}))
|
||||
Reference in New Issue
Block a user