go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/providers-sdk/v1/lr/go.go (about) 1 // Copyright (c) Mondoo, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 package lr 5 6 import ( 7 "bytes" 8 "errors" 9 "fmt" 10 "path/filepath" 11 "regexp" 12 "strconv" 13 "strings" 14 15 "go.mondoo.com/cnquery/types" 16 "go.mondoo.com/cnquery/utils/multierr" 17 ) 18 19 // Go produced go code for the LR file 20 func Go(packageName string, ast *LR, collector *Collector) (string, error) { 21 o := goBuilder{ 22 collector: collector, 23 ast: ast, 24 name: packageName, 25 providerName: filepath.Base(ast.Options["provider"]), 26 packsInUse: map[string]struct{}{}, 27 } 28 29 // should generally not happen and is primarily used for logging 30 if o.providerName == "" { 31 o.providerName = "provider" 32 } 33 34 o.goCreateResource(ast.Resources) 35 o.goGetData(ast.Resources) 36 o.goSetData(ast.Resources) 37 38 for i := range ast.Resources { 39 o.goResource(ast.Resources[i]) 40 } 41 42 imports := "" 43 for packName := range o.packsInUse { 44 importPath, ok := ast.packPaths[packName] 45 if !ok { 46 return "", errors.New("cannot find import path for pack: " + packName) 47 } 48 49 imports += "\n\t" + strconv.Quote(importPath) 50 } 51 52 coreImports := "" 53 if hasTimeImports(ast, &o) { 54 coreImports = "\n\t\"time\"" 55 } 56 57 header := fmt.Sprintf(goHeader, coreImports, imports) 58 return header + o.data, o.errors.Deduplicate() 59 } 60 61 func hasTimeImports(ast *LR, b *goBuilder) bool { 62 timeType := primitiveTypes["time"] 63 for i := range ast.Resources { 64 r := ast.Resources[i] 65 for j := range r.Body.Fields { 66 field := r.Body.Fields[j] 67 if field.BasicField != nil && field.BasicField.Type.goType(b) == timeType { 68 return true 69 } 70 } 71 } 72 return false 73 } 74 75 var reservedKeywords = map[string]struct{}{} 76 77 func init() { 78 // Reserved keywords up to go 1.20 79 words := ` 80 break default func interface select 81 case defer go map struct 82 chan else goto package switch 83 const fallthrough if range type 84 continue for import return var 85 ` 86 re := regexp.MustCompile(`\S+`) 87 terms := re.FindAllString(words, -1) 88 for _, term := range terms { 89 reservedKeywords[term] = struct{}{} 90 } 91 } 92 93 func fieldCall(f string) string { 94 if _, ok := reservedKeywords[f]; ok { 95 return "compute_" + f 96 } 97 return f 98 } 99 100 type goBuilder struct { 101 data string 102 collector *Collector 103 ast *LR 104 errors multierr.Errors 105 name string 106 providerName string 107 packsInUse map[string]struct{} 108 } 109 110 const goHeader = `// Copyright (c) Mondoo, Inc. 111 // SPDX-License-Identifier: BUSL-1.1 112 113 // Code generated by resources. DO NOT EDIT. 114 115 package resources 116 117 import ( 118 "errors"%s 119 120 "go.mondoo.com/cnquery/llx" 121 "go.mondoo.com/cnquery/providers-sdk/v1/plugin" 122 "go.mondoo.com/cnquery/types"%s 123 ) 124 ` 125 126 func (b *goBuilder) goCreateResource(r []*Resource) { 127 newCmds := make([]string, len(r)) 128 for i := range r { 129 resource := r[i] 130 iName := resource.interfaceName(b) 131 132 var parseArgs string 133 if b.collector.HasInit(iName) { 134 parseArgs = "Init: init" + iName + "," 135 } else { 136 parseArgs = "// to override args, implement: init" + iName + "(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error)" 137 } 138 139 newCmds[i] = fmt.Sprintf("\"%s\": {\n\t\t\t%s\n\t\t\tCreate: create%s,\n\t\t},", resource.ID, parseArgs, iName) 140 } 141 142 b.data += ` 143 var resourceFactories map[string]plugin.ResourceFactory 144 145 func init() { 146 resourceFactories = map[string]plugin.ResourceFactory { 147 ` + strings.Join(newCmds, "\n\t\t") + ` 148 } 149 } 150 151 // NewResource is used by the runtime of this plugin to create new resources. 152 // Its arguments may be provided by users. This function is generally not 153 // used by initializing resources from recordings or from lists. 154 func NewResource(runtime *plugin.Runtime, name string, args map[string]*llx.RawData) (plugin.Resource, error) { 155 f, ok := resourceFactories[name] 156 if !ok { 157 return nil, errors.New("cannot find resource " + name + " in this provider") 158 } 159 160 if f.Init != nil { 161 cargs, res, err := f.Init(runtime, args) 162 if err != nil { 163 return res, err 164 } 165 166 if res != nil { 167 id := name+"\x00"+res.MqlID() 168 if x, ok := runtime.Resources.Get(id); ok { 169 return x, nil 170 } 171 runtime.Resources.Set(id, res) 172 return res, nil 173 } 174 175 args = cargs 176 } 177 178 res, err := f.Create(runtime, args) 179 if err != nil { 180 return nil, err 181 } 182 183 id := name+"\x00"+res.MqlID() 184 if x, ok := runtime.Resources.Get(id); ok { 185 return x, nil 186 } 187 188 runtime.Resources.Set(id, res) 189 return res, nil 190 } 191 192 // CreateResource is used by the runtime of this plugin to create resources. 193 // Its arguments must be complete and pre-processed. This method is used 194 // for initializing resources from recordings or from lists. 195 func CreateResource(runtime *plugin.Runtime, name string, args map[string]*llx.RawData) (plugin.Resource, error) { 196 f, ok := resourceFactories[name] 197 if !ok { 198 return nil, errors.New("cannot find resource " + name + " in this provider") 199 } 200 201 res, err := f.Create(runtime, args) 202 if err != nil { 203 return nil, err 204 } 205 206 id := name+"\x00"+res.MqlID() 207 if x, ok := runtime.Resources.Get(id); ok { 208 return x, nil 209 } 210 211 runtime.Resources.Set(id, res) 212 return res, nil 213 } 214 ` 215 } 216 217 func (b *goBuilder) goGetData(r []*Resource) { 218 fields := []string{} 219 for i := range r { 220 resource := r[i] 221 for j := range resource.Body.Fields { 222 field := resource.Body.Fields[j] 223 if field.Init != nil { 224 continue 225 } 226 227 x := fmt.Sprintf(`"%s.%s": func(r plugin.Resource) *plugin.DataRes { 228 return (r.(*%s).Get%s()).ToDataRes(%s) 229 },`, 230 resource.ID, field.BasicField.ID, 231 resource.structName(b), field.BasicField.methodname(), 232 field.BasicField.Type.mondooType(b), 233 ) 234 fields = append(fields, x) 235 } 236 } 237 238 b.data += ` 239 var getDataFields = map[string]func(r plugin.Resource) *plugin.DataRes{ 240 ` + strings.Join(fields, "\n\t") + ` 241 } 242 243 func GetData(resource plugin.Resource, field string, args map[string]*llx.RawData) *plugin.DataRes { 244 f, ok := getDataFields[resource.MqlName()+"."+field] 245 if !ok { 246 return &plugin.DataRes{Error: "cannot find '" + field + "' in resource '" + resource.MqlName() + "'"} 247 } 248 249 return f(resource) 250 } 251 ` 252 } 253 254 func (b *goBuilder) goSetData(r []*Resource) { 255 fields := []string{} 256 for i := range r { 257 resource := r[i] 258 259 x := fmt.Sprintf(`"%s.%s": func(r plugin.Resource, v *llx.RawData) (ok bool) { 260 r.(*%s).__id, ok = v.Value.(string) 261 return 262 },`, 263 resource.ID, "__id", 264 resource.structName(b), 265 ) 266 fields = append(fields, x) 267 268 for j := range resource.Body.Fields { 269 field := resource.Body.Fields[j] 270 if field.Init != nil { 271 continue 272 } 273 274 x := fmt.Sprintf(`"%s.%s": func(r plugin.Resource, v *llx.RawData) (ok bool) { 275 r.(*%s).%s, ok = plugin.RawToTValue[%s](v.Value, v.Error) 276 return 277 },`, 278 resource.ID, field.BasicField.ID, 279 resource.structName(b), field.BasicField.methodname(), 280 field.BasicField.Type.goType(b), 281 ) 282 fields = append(fields, x) 283 } 284 } 285 286 b.data += fmt.Sprintf(` 287 var setDataFields = map[string]func(r plugin.Resource, v *llx.RawData) bool { 288 %s 289 } 290 291 func SetData(resource plugin.Resource, field string, val *llx.RawData) error { 292 f, ok := setDataFields[resource.MqlName() + "." + field] 293 if !ok { 294 return errors.New("[%s] cannot set '"+field+"' in resource '"+resource.MqlName()+"', field not found") 295 } 296 297 if ok := f(resource, val); !ok { 298 return errors.New("[%s] cannot set '"+field+"' in resource '"+resource.MqlName()+"', type does not match") 299 } 300 return nil 301 } 302 303 func SetAllData(resource plugin.Resource, args map[string]*llx.RawData) error { 304 var err error 305 for k, v := range args { 306 if err = SetData(resource, k, v); err != nil { 307 return err 308 } 309 } 310 return nil 311 } 312 `, 313 strings.Join(fields, "\n\t"), 314 b.providerName, b.providerName, 315 ) 316 } 317 318 func (b *goBuilder) goResource(r *Resource) { 319 b.goStruct(r) 320 b.goFactory(r) 321 b.goFields(r) 322 } 323 324 func (b *goBuilder) goStruct(r *Resource) { 325 internalStruct := r.structName(b) + "Internal" 326 if !b.collector.HasStruct(internalStruct) { 327 internalStruct = "// optional: if you define " + internalStruct + " it will be used here" 328 } 329 330 fields := []string{} 331 for i := range r.Body.Fields { 332 field := r.Body.Fields[i] 333 if field.Init != nil { 334 continue 335 } 336 fields = append(fields, field.BasicField.goName()+" plugin.TValue["+field.BasicField.Type.goType(b)+"]") 337 } 338 339 sFields := strings.Join(fields, "\n\t") 340 if len(fields) != 0 { 341 sFields = "\n\t" + sFields 342 } 343 344 b.data += fmt.Sprintf(` 345 // %s for the %s resource 346 type %s struct { 347 MqlRuntime *plugin.Runtime 348 __id string 349 %s%s 350 } 351 `, 352 r.structName(b), r.ID, r.structName(b), 353 internalStruct, 354 sFields, 355 ) 356 } 357 358 func (b *goBuilder) goFactory(r *Resource) { 359 createName := "create" + r.interfaceName(b) 360 structName := r.structName(b) 361 362 var idCode string 363 if b.collector.HasID(structName) { 364 idCode = `if res.__id == "" { 365 res.__id, err = res.id() 366 if err != nil { 367 return nil, err 368 } 369 }` 370 } else { 371 idCode = `// to override __id implement: id() (string, error)` 372 } 373 374 b.data += fmt.Sprintf(` 375 // %s creates a new instance of this resource 376 func %s(runtime *plugin.Runtime, args map[string]*llx.RawData) (plugin.Resource, error) { 377 res := &%s{ 378 MqlRuntime: runtime, 379 } 380 381 err := SetAllData(res, args) 382 if err != nil { 383 return res, err 384 } 385 386 %s 387 388 if runtime.HasRecording { 389 args, err = runtime.ResourceFromRecording(%s, res.__id) 390 if err != nil || args == nil { 391 return res, err 392 } 393 return res, SetAllData(res, args) 394 } 395 396 return res, nil 397 } 398 399 func (c *%s) MqlName() string { 400 return "%s" 401 } 402 403 func (c *%s) MqlID() string { 404 return c.__id 405 } 406 `, 407 createName, createName, 408 structName, 409 idCode, 410 strconv.Quote(r.ID), 411 structName, r.ID, 412 structName, 413 ) 414 } 415 416 func (b *goBuilder) goFields(r *Resource) { 417 for i := range r.Body.Fields { 418 field := r.Body.Fields[i] 419 if field.Init != nil { 420 continue 421 } 422 423 b.goField(r, field) 424 } 425 } 426 427 func (b *goBuilder) goStaticField(r *Resource, field *Field) { 428 goName := field.BasicField.goName() 429 b.data += fmt.Sprintf(` 430 func (c *%s) Get%s() *plugin.TValue[%s] { 431 return &c.%s 432 } 433 `, 434 r.structName(b), goName, field.BasicField.Type.goType(b), 435 goName, 436 ) 437 } 438 439 func (b *goBuilder) goField(r *Resource, field *Field) { 440 if field.BasicField.isStatic() { 441 b.goStaticField(r, field) 442 return 443 } 444 445 if field.BasicField.ID == "id" { 446 b.errors.Add(errors.New("cannot create a dynamically computed `id` field, please turn it into a static field (ie: `id(..)` => `id`)")) 447 return 448 } 449 450 goName := field.BasicField.goName() 451 goType := field.BasicField.Type.goType(b) 452 goZero := field.BasicField.Type.goZeroValue() 453 454 argDefs := []string{} 455 argCall := []string{} 456 if field.BasicField.Args != nil { 457 args := field.BasicField.Args.List 458 for i := range args { 459 arg := args[i] 460 name := resource2goname(arg.Type, b) 461 argDefs = append(argDefs, fmt.Sprintf(`varg%s := c.Get%s() 462 if varg%s.Error != nil { 463 return %s, varg%s.Error 464 } 465 466 `, name, name, name, goZero, name)) 467 argCall = append(argCall, "varg"+name+".Data") 468 } 469 } 470 471 // resource types may be loaded from recordings 472 var fromRecording string 473 if field.BasicField.Type.containsResource(b) { 474 fromRecording = fmt.Sprintf(`if c.MqlRuntime.HasRecording { 475 d, err := c.MqlRuntime.FieldResourceFromRecording(%s, c.__id, %s) 476 if err != nil { 477 return %s, err 478 } 479 if d != nil { 480 return d.Value.(%s), nil 481 } 482 } 483 484 `, 485 strconv.Quote(r.ID), strconv.Quote(field.BasicField.ID), 486 goZero, 487 goType, 488 ) 489 } 490 491 b.data += fmt.Sprintf(` 492 func (c *%s) Get%s() *plugin.TValue[%s] { 493 return plugin.GetOrCompute[%s](&c.%s, func() (%s, error) { 494 %s%sreturn c.%s(%s) 495 }) 496 } 497 `, 498 r.structName(b), goName, goType, 499 goType, goName, goType, 500 fromRecording, strings.Join(argDefs, ""), 501 fieldCall(field.BasicField.ID), strings.Join(argCall, ", "), 502 ) 503 } 504 505 // GO METHODS FOR AST 506 507 func (b *goBuilder) importName(symbol string) (string, bool) { 508 parts := strings.SplitN(symbol, ".", 2) 509 if len(parts) >= 2 { 510 if ref, ok := b.ast.imports[parts[0]]; ok { 511 if _, ok := ref[parts[1]]; ok { 512 return parts[1], true 513 } 514 } 515 } 516 return "", false 517 } 518 519 func indent(s string, depth int) string { 520 space := "" 521 for i := 0; i < depth; i++ { 522 space += "\t" 523 } 524 return space + strings.Replace(s, "\n", "\n"+space, -1) 525 } 526 527 func (r *Resource) structName(b *goBuilder) string { 528 return "mql" + r.interfaceName(b) 529 } 530 531 var reMethodName = regexp.MustCompile("\\.[a-z]") 532 533 func capitalizeDot(in []byte) []byte { 534 return bytes.ToUpper([]byte{in[1]}) 535 } 536 537 func (r *Resource) interfaceName(b *goBuilder) string { 538 return resource2goname(r.ID, b) 539 } 540 541 func resource2goname(s string, b *goBuilder) string { 542 pack := strings.SplitN(s, ".", 2) 543 var name string 544 if pack[0] != s { 545 resources, ok := b.ast.imports[pack[0]] 546 if ok { 547 if _, ok := resources[pack[1]]; !ok { 548 b.errors.Add(errors.New("cannot find resource " + pack[1] + " in imported resource pack " + pack[0])) 549 } 550 name = pack[0] + "." + strings.Title(string( 551 reMethodName.ReplaceAllFunc([]byte(pack[1]), capitalizeDot), 552 )) 553 b.packsInUse[pack[0]] = struct{}{} 554 } 555 } 556 if name == "" { 557 name = strings.Title(string( 558 reMethodName.ReplaceAllFunc([]byte(s), capitalizeDot), 559 )) 560 } 561 562 return name 563 } 564 565 func (b *ResourceDef) staticFields() []*BasicField { 566 res := []*BasicField{} 567 for _, f := range b.Fields { 568 if f.BasicField != nil { 569 if f.BasicField.isStatic() { 570 res = append(res, f.BasicField) 571 } 572 } 573 } 574 return res 575 } 576 577 func (f *BasicField) goName() string { 578 return strings.Title(f.ID) 579 } 580 581 func (f *BasicField) isStatic() bool { 582 return f.Args == nil 583 } 584 585 func (f *BasicField) methodname() string { 586 return strings.Title(f.ID) 587 } 588 589 // Retrieve the raw mondoo equivalent type, which can be looked up 590 // as a resource. 591 func (t *Type) Type(ast *LR) types.Type { 592 if t.SimpleType != nil { 593 return t.SimpleType.typeItems(ast) 594 } 595 if t.ListType != nil { 596 return t.ListType.typeItems(ast) 597 } 598 if t.MapType != nil { 599 return t.MapType.typeItems(ast) 600 } 601 return types.Any 602 } 603 604 func (t *MapType) typeItems(ast *LR) types.Type { 605 return types.Map(t.Key.typeItems(ast), t.Value.Type(ast)) 606 } 607 608 func (t *ListType) typeItems(ast *LR) types.Type { 609 return types.Array(t.Type.Type(ast)) 610 } 611 612 func (t *SimpleType) typeItems(ast *LR) types.Type { 613 switch t.Type { 614 case "bool": 615 return types.Bool 616 case "int": 617 return types.Int 618 case "float": 619 return types.Float 620 case "string": 621 return types.String 622 case "regex": 623 return types.Regex 624 case "time": 625 return types.Time 626 case "dict": 627 return types.Dict 628 default: 629 return resourceType(t.Type, ast) 630 } 631 } 632 633 // Try to build an MQL resource from the given name. It may or may not exist in 634 // a pack. If it doesn't exist at all 635 func resourceType(name string, ast *LR) types.Type { 636 pack := strings.SplitN(name, ".", 2) 637 if pack[0] != name { 638 resources, ok := ast.imports[pack[0]] 639 if ok { 640 if _, ok := resources[pack[1]]; ok { 641 return types.Resource(pack[1]) 642 } 643 } 644 } 645 646 // TODO: look up resources in the current registry and notify if they are not found 647 648 return types.Resource(name) 649 } 650 651 // Retrieve the mondoo equivalent of the type. This is a stringified type 652 // i.e. it can be compiled with the MQL imports 653 func (t *Type) mondooType(b *goBuilder) string { 654 i := t.mondooTypeItems(b) 655 if i == "" { 656 return "NO_TYPE_DETECTED" 657 } 658 return i 659 } 660 661 func (t *Type) mondooTypeItems(b *goBuilder) string { 662 if t.SimpleType != nil { 663 return t.SimpleType.mondooTypeItems(b) 664 } 665 if t.ListType != nil { 666 return t.ListType.mondooTypeItems(b) 667 } 668 if t.MapType != nil { 669 return t.MapType.mondooTypeItems(b) 670 } 671 return "" 672 } 673 674 func (t *MapType) mondooTypeItems(b *goBuilder) string { 675 return "types.Map(" + t.Key.mondooTypeItems(b) + ", " + t.Value.mondooTypeItems(b) + ")" 676 } 677 678 func (t *ListType) mondooTypeItems(b *goBuilder) string { 679 return "types.Array(" + t.Type.mondooTypeItems(b) + ")" 680 } 681 682 func (t *SimpleType) mondooTypeItems(b *goBuilder) string { 683 switch t.Type { 684 case "bool": 685 return "types.Bool" 686 case "int": 687 return "types.Int" 688 case "float": 689 return "types.Float" 690 case "string": 691 return "types.String" 692 case "regex": 693 return "types.Regex" 694 case "time": 695 return "types.Time" 696 case "dict": 697 return "types.Dict" 698 default: 699 if name, ok := b.importName(t.Type); ok { 700 return "types.Resource(\"" + name + "\")" 701 } 702 return "types.Resource(\"" + t.Type + "\")" 703 } 704 705 // TODO: check that this type if a proper resource 706 // panic("Cannot convert type '" + t.Type + "' to mondoo type") 707 } 708 709 func (t *Type) containsResource(b *goBuilder) bool { 710 if t.ListType != nil { 711 return t.ListType.Type.containsResource(b) 712 } 713 if t.MapType != nil { 714 return t.MapType.Value.containsResource(b) 715 } 716 if t.SimpleType != nil { 717 if _, ok := primitiveTypes[t.SimpleType.Type]; !ok { 718 return true 719 } 720 } 721 return false 722 } 723 724 // The go type is the golang-equivalent code type, i.e. the type of the 725 // actual objects that are being moved around. 726 func (t *Type) goType(b *goBuilder) string { 727 if t.SimpleType != nil { 728 return t.SimpleType.goType(b) 729 } 730 if t.ListType != nil { 731 return t.ListType.goType() 732 } 733 if t.MapType != nil { 734 return t.MapType.goType(b) 735 } 736 return "NO_TYPE_DETECTED" 737 } 738 739 func (t *MapType) goType(b *goBuilder) string { 740 // limited to interface{} because we cannot cast as universally 741 // between types yet 742 return "map[" + t.Key.goType(b) + "]interface{}" 743 } 744 745 func (t *ListType) goType() string { 746 // limited to []interface{} because we cannot cast as universally 747 // between types yet 748 return "[]interface{}" 749 } 750 751 var primitiveTypes = map[string]string{ 752 "string": "string", 753 "bool": "bool", 754 "int": "int64", 755 "float": "float64", 756 "time": "*time.Time", 757 "regex": "string", 758 "dict": "interface{}", 759 "any": "interface{}", 760 } 761 762 func (t *SimpleType) goType(b *goBuilder) string { 763 pt, ok := primitiveTypes[t.Type] 764 if ok { 765 return pt 766 } 767 768 if _, ok := b.importName(t.Type); ok { 769 return "plugin.Resource" 770 } 771 772 return "*mql" + resource2goname(t.Type, b) 773 } 774 775 func (t *Type) goZeroValue() string { 776 if t.SimpleType != nil { 777 return t.SimpleType.goZeroValue() 778 } 779 return "nil" 780 } 781 782 var primitiveZeros = map[string]string{ 783 "string": "\"\"", 784 "bool": "false", 785 "int": "0", 786 "float": "0.0", 787 "time": "nil", 788 "dict": "nil", 789 "any": "nil", 790 } 791 792 func (t *SimpleType) goZeroValue() string { 793 pt, ok := primitiveZeros[t.Type] 794 if ok { 795 return pt 796 } 797 798 // TODO: check if the resource exists 799 return "nil" 800 }