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(©Config) 326 return rest.UnversionedRESTClientFor(©Config) 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 }