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:
iyear
2023-04-25 10:30:23 +08:00
committed by GitHub
parent 434cd4c2d0
commit 5b8c38ad3e
5 changed files with 545 additions and 0 deletions

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

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

View 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

View 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: {
...
}
}

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