github.com/kubevela/workflow@v0.6.0/pkg/cue/packages/package.go (about)

     1  /*
     2  Copyright 2022 The KubeVela Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package packages
    18  
    19  import (
    20  	"fmt"
    21  	"path/filepath"
    22  	"strings"
    23  	"sync"
    24  	"time"
    25  
    26  	"cuelang.org/go/cue"
    27  	"cuelang.org/go/cue/ast"
    28  	"cuelang.org/go/cue/build"
    29  	"cuelang.org/go/cue/cuecontext"
    30  	"cuelang.org/go/cue/parser"
    31  	"cuelang.org/go/cue/token"
    32  	"cuelang.org/go/encoding/jsonschema"
    33  	"github.com/pkg/errors"
    34  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    35  	"k8s.io/apimachinery/pkg/runtime"
    36  	"k8s.io/apimachinery/pkg/runtime/serializer"
    37  	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
    38  	"k8s.io/client-go/rest"
    39  
    40  	"github.com/kubevela/workflow/pkg/stdlib"
    41  )
    42  
    43  const (
    44  	// BuiltinPackageDomain Specify the domain of the built-in package
    45  	BuiltinPackageDomain = "kube"
    46  	// K8sResourcePrefix Indicates that the definition comes from kubernetes
    47  	K8sResourcePrefix = "io_k8s_api_"
    48  
    49  	// ParseJSONSchemaErr describes the error that occurs when cue parses json
    50  	ParseJSONSchemaErr ParseErrType = "parse json schema of k8s crds error"
    51  )
    52  
    53  // PackageDiscover defines the inner CUE packages loaded from K8s cluster
    54  type PackageDiscover struct {
    55  	velaBuiltinPackages []*build.Instance
    56  	pkgKinds            map[string][]VersionKind
    57  	mutex               sync.RWMutex
    58  	client              *rest.RESTClient
    59  }
    60  
    61  // VersionKind contains the resource metadata and reference name
    62  type VersionKind struct {
    63  	DefinitionName string
    64  	APIVersion     string
    65  	Kind           string
    66  }
    67  
    68  // ParseErrType represents the type of CUEParseError
    69  type ParseErrType string
    70  
    71  // CUEParseError describes an error when CUE parse error
    72  type CUEParseError struct {
    73  	err     error
    74  	errType ParseErrType
    75  }
    76  
    77  // Error implements the Error interface.
    78  func (cueErr CUEParseError) Error() string {
    79  	return fmt.Sprintf("%s: %s", cueErr.errType, cueErr.err.Error())
    80  }
    81  
    82  // IsCUEParseErr returns true if the specified error is CUEParseError type.
    83  func IsCUEParseErr(err error) bool {
    84  	return errors.As(err, &CUEParseError{})
    85  }
    86  
    87  // NewPackageDiscover will create a PackageDiscover client with the K8s config file.
    88  func NewPackageDiscover(config *rest.Config) (*PackageDiscover, error) {
    89  	client, err := getClusterOpenAPIClient(config)
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  	pd := &PackageDiscover{
    94  		client:   client,
    95  		pkgKinds: make(map[string][]VersionKind),
    96  	}
    97  	if err = pd.RefreshKubePackagesFromCluster(); err != nil {
    98  		return pd, err
    99  	}
   100  	return pd, nil
   101  }
   102  
   103  // ImportBuiltinPackagesFor will add KubeVela built-in packages into your CUE instance
   104  func (pd *PackageDiscover) ImportBuiltinPackagesFor(bi *build.Instance) {
   105  	pd.mutex.RLock()
   106  	defer pd.mutex.RUnlock()
   107  	bi.Imports = append(bi.Imports, pd.velaBuiltinPackages...)
   108  }
   109  
   110  // ImportPackagesAndBuildInstance Combine import built-in packages and build cue template together to avoid data race
   111  // nolint:staticcheck
   112  func (pd *PackageDiscover) ImportPackagesAndBuildInstance(bi *build.Instance) (inst *cue.Instance, err error) {
   113  	var r cue.Runtime
   114  	if pd == nil {
   115  		return r.Build(bi)
   116  	}
   117  	pd.ImportBuiltinPackagesFor(bi)
   118  	if err := stdlib.AddImportsFor(bi, ""); err != nil {
   119  		return nil, err
   120  	}
   121  	pd.mutex.Lock()
   122  	defer pd.mutex.Unlock()
   123  	return r.Build(bi)
   124  }
   125  
   126  // ImportPackagesAndBuildValue Combine import built-in packages and build cue template together to avoid data race
   127  func (pd *PackageDiscover) ImportPackagesAndBuildValue(bi *build.Instance) (val cue.Value, err error) {
   128  	cuectx := cuecontext.New()
   129  	if pd == nil {
   130  		return cuectx.BuildInstance(bi), nil
   131  	}
   132  	pd.ImportBuiltinPackagesFor(bi)
   133  	if err := stdlib.AddImportsFor(bi, ""); err != nil {
   134  		return cue.Value{}, err
   135  	}
   136  	pd.mutex.Lock()
   137  	defer pd.mutex.Unlock()
   138  	return cuectx.BuildInstance(bi), nil
   139  }
   140  
   141  // ListPackageKinds list packages and their kinds
   142  func (pd *PackageDiscover) ListPackageKinds() map[string][]VersionKind {
   143  	pd.mutex.RLock()
   144  	defer pd.mutex.RUnlock()
   145  	return pd.pkgKinds
   146  }
   147  
   148  // RefreshKubePackagesFromCluster will use K8s client to load/refresh all K8s open API as a reference kube package using in template
   149  func (pd *PackageDiscover) RefreshKubePackagesFromCluster() error {
   150  	return nil
   151  	// body, err := pd.client.Get().AbsPath("/openapi/v2").Do(context.Background()).Raw()
   152  	// if err != nil {
   153  	//	 return err
   154  	// }
   155  	// return pd.addKubeCUEPackagesFromCluster(string(body))
   156  }
   157  
   158  // Exist checks if the GVK exists in the built-in packages
   159  func (pd *PackageDiscover) Exist(gvk metav1.GroupVersionKind) bool {
   160  	dgvk := convert2DGVK(gvk)
   161  	// package name equals to importPath
   162  	importPath := genStandardPkgName(dgvk)
   163  	pd.mutex.RLock()
   164  	defer pd.mutex.RUnlock()
   165  	pkgKinds, ok := pd.pkgKinds[importPath]
   166  	if !ok {
   167  		pkgKinds = pd.pkgKinds[genOpenPkgName(dgvk)]
   168  	}
   169  	for _, v := range pkgKinds {
   170  		if v.Kind == dgvk.Kind {
   171  			return true
   172  		}
   173  	}
   174  	return false
   175  }
   176  
   177  // mount will mount the new parsed package into PackageDiscover built-in packages
   178  func (pd *PackageDiscover) mount(pkg *pkgInstance, pkgKinds []VersionKind) {
   179  	pd.mutex.Lock()
   180  	defer pd.mutex.Unlock()
   181  	if pkgKinds == nil {
   182  		pkgKinds = []VersionKind{}
   183  	}
   184  	for i, p := range pd.velaBuiltinPackages {
   185  		if p.ImportPath == pkg.ImportPath {
   186  			pd.pkgKinds[pkg.ImportPath] = pkgKinds
   187  			pd.velaBuiltinPackages[i] = pkg.Instance
   188  			return
   189  		}
   190  	}
   191  	pd.pkgKinds[pkg.ImportPath] = pkgKinds
   192  	pd.velaBuiltinPackages = append(pd.velaBuiltinPackages, pkg.Instance)
   193  }
   194  
   195  func (pd *PackageDiscover) pkgBuild(packages map[string]*pkgInstance, pkgName string,
   196  	dGVK domainGroupVersionKind, def string, kubePkg *pkgInstance, groupKinds map[string][]VersionKind) error {
   197  	pkg, ok := packages[pkgName]
   198  	if !ok {
   199  		pkg = newPackage(pkgName)
   200  		pkg.Imports = []*build.Instance{kubePkg.Instance}
   201  	}
   202  
   203  	mykinds := groupKinds[pkgName]
   204  	mykinds = append(mykinds, VersionKind{
   205  		APIVersion:     dGVK.APIVersion,
   206  		Kind:           dGVK.Kind,
   207  		DefinitionName: "#" + dGVK.Kind,
   208  	})
   209  
   210  	file, err := parser.ParseFile(dGVK.reverseString(), def)
   211  	if err != nil {
   212  		return err
   213  	}
   214  	if err := pkg.AddSyntax(file); err != nil {
   215  		return err
   216  	}
   217  
   218  	packages[pkgName] = pkg
   219  	groupKinds[pkgName] = mykinds
   220  	return nil
   221  }
   222  
   223  func (pd *PackageDiscover) addKubeCUEPackagesFromCluster(apiSchema string) error {
   224  	file, err := parser.ParseFile("-", apiSchema)
   225  	if err != nil {
   226  		return err
   227  	}
   228  	oaInst := cuecontext.New().BuildFile(file)
   229  	if err != nil {
   230  		return err
   231  	}
   232  	dgvkMapper := make(map[string]domainGroupVersionKind)
   233  	pathValue := oaInst.LookupPath(cue.ParsePath("paths"))
   234  	if pathValue.Exists() {
   235  		iter, err := pathValue.Fields()
   236  		if err != nil {
   237  			return err
   238  		}
   239  		for iter.Next() {
   240  			gvk := iter.Value().LookupPath(cue.ParsePath("post[\"x-kubernetes-group-version-kind\"]"))
   241  			if gvk.Exists() {
   242  				if v, err := getDGVK(gvk); err == nil {
   243  					dgvkMapper[v.reverseString()] = v
   244  				}
   245  			}
   246  		}
   247  	}
   248  	oaFile, err := jsonschema.Extract(oaInst, &jsonschema.Config{
   249  		Root: "#/definitions",
   250  		Map:  openAPIMapping(dgvkMapper),
   251  	})
   252  	if err != nil {
   253  		return CUEParseError{
   254  			err:     err,
   255  			errType: ParseJSONSchemaErr,
   256  		}
   257  	}
   258  	kubePkg := newPackage("kube")
   259  	kubePkg.processOpenAPIFile(oaFile)
   260  	if err := kubePkg.AddSyntax(oaFile); err != nil {
   261  		return err
   262  	}
   263  	packages := make(map[string]*pkgInstance)
   264  	groupKinds := make(map[string][]VersionKind)
   265  
   266  	for k := range dgvkMapper {
   267  		v := dgvkMapper[k]
   268  		apiVersion := v.APIVersion
   269  		def := fmt.Sprintf(`
   270  import "kube"
   271  
   272  #%s: kube.%s & {
   273  kind: "%s"
   274  apiVersion: "%s",
   275  }`, v.Kind, k, v.Kind, apiVersion)
   276  
   277  		if err := pd.pkgBuild(packages, genStandardPkgName(v), v, def, kubePkg, groupKinds); err != nil {
   278  			return err
   279  		}
   280  		if err := pd.pkgBuild(packages, genOpenPkgName(v), v, def, kubePkg, groupKinds); err != nil {
   281  			return err
   282  		}
   283  	}
   284  	for name, pkg := range packages {
   285  		pd.mount(pkg, groupKinds[name])
   286  	}
   287  	return nil
   288  }
   289  
   290  func genOpenPkgName(v domainGroupVersionKind) string {
   291  	return BuiltinPackageDomain + "/" + v.APIVersion
   292  }
   293  
   294  func genStandardPkgName(v domainGroupVersionKind) string {
   295  	res := []string{v.Group, v.Version}
   296  	if v.Domain != "" {
   297  		res = []string{v.Domain, v.Group, v.Version}
   298  	}
   299  
   300  	return strings.Join(res, "/")
   301  }
   302  
   303  func setDiscoveryDefaults(config *rest.Config) {
   304  	config.APIPath = ""
   305  	config.GroupVersion = nil
   306  	if config.Timeout == 0 {
   307  		config.Timeout = 32 * time.Second
   308  	}
   309  	if config.Burst == 0 && config.QPS < 100 {
   310  		// discovery is expected to be bursty, increase the default burst
   311  		// to accommodate looking up resource info for many API groups.
   312  		// matches burst set by ConfigFlags#ToDiscoveryClient().
   313  		// see https://issue.k8s.io/86149
   314  		config.Burst = 100
   315  	}
   316  	codec := runtime.NoopEncoder{Decoder: clientgoscheme.Codecs.UniversalDecoder()}
   317  	config.NegotiatedSerializer = serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{Serializer: codec})
   318  	if len(config.UserAgent) == 0 {
   319  		config.UserAgent = rest.DefaultKubernetesUserAgent()
   320  	}
   321  }
   322  
   323  func getClusterOpenAPIClient(config *rest.Config) (*rest.RESTClient, error) {
   324  	copyConfig := *config
   325  	setDiscoveryDefaults(&copyConfig)
   326  	return rest.UnversionedRESTClientFor(&copyConfig)
   327  }
   328  
   329  func openAPIMapping(dgvkMapper map[string]domainGroupVersionKind) func(pos token.Pos, a []string) ([]ast.Label, error) {
   330  	return func(pos token.Pos, a []string) ([]ast.Label, error) {
   331  		if len(a) < 2 {
   332  			return nil, errors.New("openAPIMapping format invalid")
   333  		}
   334  
   335  		name := strings.ReplaceAll(a[1], ".", "_")
   336  		name = strings.ReplaceAll(name, "-", "_")
   337  		if _, ok := dgvkMapper[name]; !ok && strings.HasPrefix(name, K8sResourcePrefix) {
   338  			trimName := strings.TrimPrefix(name, K8sResourcePrefix)
   339  			if v, ok := dgvkMapper[trimName]; ok {
   340  				v.Domain = "k8s.io"
   341  				dgvkMapper[name] = v
   342  				delete(dgvkMapper, trimName)
   343  			}
   344  		}
   345  
   346  		if strings.HasSuffix(a[1], ".JSONSchemaProps") && pos != token.NoPos {
   347  			return []ast.Label{ast.NewIdent("_")}, nil
   348  		}
   349  
   350  		return []ast.Label{ast.NewIdent(name)}, nil
   351  	}
   352  
   353  }
   354  
   355  type domainGroupVersionKind struct {
   356  	Domain     string
   357  	Group      string
   358  	Version    string
   359  	Kind       string
   360  	APIVersion string
   361  }
   362  
   363  func (dgvk domainGroupVersionKind) reverseString() string {
   364  	var s = []string{dgvk.Kind, dgvk.Version}
   365  	s = append(s, strings.Split(dgvk.Group, ".")...)
   366  	domain := dgvk.Domain
   367  	if domain == "k8s.io" {
   368  		domain = "api.k8s.io"
   369  	}
   370  
   371  	if domain != "" {
   372  		s = append(s, strings.Split(domain, ".")...)
   373  	}
   374  
   375  	for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
   376  		s[i], s[j] = s[j], s[i]
   377  	}
   378  	return strings.ReplaceAll(strings.Join(s, "_"), "-", "_")
   379  }
   380  
   381  type pkgInstance struct {
   382  	*build.Instance
   383  }
   384  
   385  func newPackage(name string) *pkgInstance {
   386  	return &pkgInstance{
   387  		&build.Instance{
   388  			PkgName:    filepath.Base(name),
   389  			ImportPath: name,
   390  		},
   391  	}
   392  }
   393  
   394  func (pkg *pkgInstance) processOpenAPIFile(f *ast.File) {
   395  	ast.Walk(f, func(node ast.Node) bool {
   396  		if st, ok := node.(*ast.StructLit); ok {
   397  			hasEllipsis := false
   398  			for index, elt := range st.Elts {
   399  				if _, isEllipsis := elt.(*ast.Ellipsis); isEllipsis {
   400  					if hasEllipsis {
   401  						st.Elts = st.Elts[:index]
   402  						return true
   403  					}
   404  					if index > 0 {
   405  						st.Elts = st.Elts[:index]
   406  						return true
   407  					}
   408  					hasEllipsis = true
   409  				}
   410  			}
   411  		}
   412  		return true
   413  	}, nil)
   414  
   415  	for _, decl := range f.Decls {
   416  		if field, ok := decl.(*ast.Field); ok {
   417  			if val, ok := field.Value.(*ast.Ident); ok && val.Name == "string" {
   418  				field.Value = ast.NewBinExpr(token.OR, ast.NewIdent("int"), ast.NewIdent("string"))
   419  			}
   420  		}
   421  	}
   422  }
   423  
   424  func getDGVK(v cue.Value) (ret domainGroupVersionKind, err error) {
   425  	gvk := metav1.GroupVersionKind{}
   426  	gvk.Group, err = v.LookupPath(cue.ParsePath("group")).String()
   427  	if err != nil {
   428  		return
   429  	}
   430  	gvk.Version, err = v.LookupPath(cue.ParsePath("version")).String()
   431  	if err != nil {
   432  		return
   433  	}
   434  
   435  	gvk.Kind, err = v.LookupPath(cue.ParsePath("kind")).String()
   436  	if err != nil {
   437  		return
   438  	}
   439  
   440  	ret = convert2DGVK(gvk)
   441  	return
   442  }
   443  
   444  func convert2DGVK(gvk metav1.GroupVersionKind) domainGroupVersionKind {
   445  	ret := domainGroupVersionKind{
   446  		Version:    gvk.Version,
   447  		Kind:       gvk.Kind,
   448  		APIVersion: gvk.Version,
   449  	}
   450  	if gvk.Group == "" {
   451  		ret.Group = "core"
   452  		ret.Domain = "k8s.io"
   453  	} else {
   454  		ret.APIVersion = gvk.Group + "/" + ret.APIVersion
   455  		sv := strings.Split(gvk.Group, ".")
   456  		// Domain must contain dot
   457  		if len(sv) > 2 {
   458  			ret.Domain = strings.Join(sv[1:], ".")
   459  			ret.Group = sv[0]
   460  		} else {
   461  			ret.Group = gvk.Group
   462  		}
   463  	}
   464  	return ret
   465  }