github.com/jenkins-x/jx/v2@v2.1.155/cmd/codegen/generator/open_api.go (about) 1 package generator 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "go/ast" 8 "go/format" 9 "go/parser" 10 "go/token" 11 "html/template" 12 "io/ioutil" 13 "os" 14 "path/filepath" 15 "strconv" 16 "strings" 17 18 "github.com/jenkins-x/jx/v2/cmd/codegen/util" 19 20 "github.com/ghodss/yaml" 21 22 "k8s.io/kube-openapi/pkg/builder" 23 24 "k8s.io/kube-openapi/pkg/common" 25 26 "github.com/go-openapi/spec" 27 "github.com/pkg/errors" 28 ) 29 30 const ( 31 openapiTemplateSrc = `// +build !ignore_autogenerated 32 33 // Code generated by jx create client. DO NOT EDIT. 34 package openapi 35 36 import ( 37 openapicore "{{ $.Path }}" 38 {{ range $i, $path := $.Dependents }} 39 openapi{{ $i }} "{{ $path }}" 40 {{ end }} 41 "k8s.io/kube-openapi/pkg/common" 42 ) 43 44 func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { 45 result := make(map[string]common.OpenAPIDefinition) 46 // This is our core openapi definitions (the ones for this module) 47 for k, v := range openapicore.GetOpenAPIDefinitions(ref) { 48 result[k] = v 49 } 50 // These are the ones we depend on 51 {{ range $i, $path := $.Dependents }} 52 for k, v := range openapi{{ $i}}.GetOpenAPIDefinitions(ref) { 53 result[k] = v 54 } 55 {{ end }} 56 return result 57 } 58 59 func GetNames(ref common.ReferenceCallback) []string { 60 result := make([]string, 0) 61 for k, _ := range openapicore.GetOpenAPIDefinitions(ref) { 62 result = append(result, k) 63 } 64 return result 65 } 66 ` 67 68 schemaWriterTemplateSrc = `package main 69 70 import ( 71 "flag" 72 "os" 73 "strings" 74 75 openapi "{{ $.AllImportPath }}" 76 77 "github.com/go-openapi/spec" 78 79 "github.com/pkg/errors" 80 81 "github.com/jenkins-x/jx/v2/cmd/codegen/generator" 82 ) 83 84 func main() { 85 var outputDir, namesStr, title, version string 86 flag.StringVar(&outputDir, "output-directory", "", "directory to write generated files to") 87 flag.StringVar(&namesStr, "names", "", "comma separated list of resources to generate schema for, "+ 88 "if empty all resources will be generated") 89 flag.StringVar(&title, "title", "", "title for OpenAPI and HTML generated docs") 90 flag.StringVar(&version, "version", "", "version for OpenAPI and HTML generated docs") 91 flag.Parse() 92 if outputDir == "" { 93 panic(errors.New("--output-directory cannot be empty")) 94 } 95 var names []string 96 if namesStr != "" { 97 names = strings.Split(namesStr, ",") 98 } else { 99 refCallback := func(path string) spec.Ref { 100 return spec.Ref{} 101 } 102 names = openapi.GetNames(refCallback) 103 } 104 err := generator.WriteSchemaToDisk(outputDir, title, version, openapi.GetOpenAPIDefinitions, names) 105 if err != nil { 106 panic(errors.Wrapf(err, "writing schema to %s", outputDir)) 107 } 108 os.Exit(0) 109 } 110 ` 111 OpenApiDir = "openapi" 112 SchemaWriterSrcFileName = "schema_writer_generated.go" 113 OpenApiV2JSON = "openapiv2.json" 114 OpenApiV2YAML = "openapiv2.yaml" 115 openApiGenerator = "openapi-gen" 116 117 bootstrapJsUrl = "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" 118 bootstrapJsFileName = "bootstrap-3.3.7.min.js" 119 jqueryUrl = "https://code.jquery.com/jquery-3.2.1.min.js" 120 jqueryFileName = "jquery-3.2.1.min.js" 121 122 openApiGen = "k8s.io/kube-openapi/cmd/openapi-gen" 123 ) 124 125 var ( 126 fonts = []string{ 127 "FontAwesome.otf", 128 "fontawesome-webfont.eot", 129 "fontawesome-webfont.svg", 130 "fontawesome-webfont.ttf", 131 "fontawesome-webfont.woff", 132 "fontawesome-webfont.woff2", 133 } 134 135 css = []string{ 136 "stylesheet.css", 137 "bootstrap.min.css", 138 "font-awesome.min.css", 139 } 140 141 js = []string{ 142 "jquery-3.2.1.min.js", 143 "bootstrap-3.3.7.min.js", 144 } 145 146 jsroot = []string{ 147 "scroll.js", 148 "jquery.scrollTo.min.js", 149 } 150 151 build = []string{ 152 "index.html", 153 "navData.js", 154 } 155 ) 156 157 type openapiTemplateData struct { 158 Dependents []string 159 Path string 160 } 161 162 type schemaWriterTemplateData struct { 163 AllImportPath string 164 } 165 166 // InstallOpenApiGen installs the openapi-gen tool from the github.com/kubernetes/kube-openapi repository. 167 func InstallOpenApiGen(version string, gopath string) error { 168 util.AppLogger().Infof("installing %s with version %s via 'go get' to %s", openApiGen, version, gopath) 169 err := util.GoGet(openApiGen, version, gopath, true, false, false) 170 if err != nil { 171 return err 172 } 173 174 return nil 175 } 176 177 // GenerateOpenApi generates the OpenAPI structs and schema files. 178 // It looks at the specified groupsWithVersions in inputPackage and generates to outputPackage ( 179 // relative to the module outputBase). Any openApiDependencies also have OpenAPI structs generated. 180 // A boilerplateFile is written to the top of any generated files. 181 // The gitter client is used to ensure the correct versions of dependencies are loaded. 182 func GenerateOpenApi(groupsWithVersions []string, inputPackage string, outputPackage string, relativePackage string, 183 outputBase string, openApiDependencies []string, moduleDir string, moduleName string, boilerplateFile string, gopath string, semVer string) error { 184 basePkg := fmt.Sprintf("%s/openapi", outputPackage) 185 corePkg := fmt.Sprintf("%s/core", basePkg) 186 allPkg := fmt.Sprintf("%s/all", basePkg) 187 188 // Generate the dependent openapi structs as these are missing from the k8s client 189 dependentPackages, err := generateOpenApiDependenciesStruct(outputPackage, relativePackage, outputBase, 190 openApiDependencies, moduleDir, moduleName, boilerplateFile, gopath) 191 if err != nil { 192 return err 193 } 194 // Generate the main openapi struct 195 err = defaultGenerate(openApiGenerator, "openapi", groupsWithVersions, inputPackage, 196 corePkg, outputBase, boilerplateFile, gopath, "--output-package", corePkg) 197 if err != nil { 198 return err 199 } 200 _, err = writeOpenApiAll(outputBase, allPkg, corePkg, dependentPackages, semVer) 201 if err != nil { 202 return err 203 } 204 _, err = writeSchemaWriterToDisk(outputBase, basePkg, allPkg, semVer) 205 if err != nil { 206 return err 207 } 208 return nil 209 } 210 211 // writeOpenApiAll code generates a file in openapi/all that reads in all the generated openapi structs and puts them 212 // in a single map, allowing them to be used by the schema writer and the CRD registration. 213 // baseDir is the root of the module, outputPackage is the base path of the output package, 214 // path is the path to the core openapi package (those that are generated for module the generator is run against), 215 // and dependents is the paths to the dependent openapi packages 216 func writeOpenApiAll(baseDir string, outputPackage string, path string, dependents []string, semVer string) (string, 217 error) { 218 tmpl, err := template.New("openapi").Parse(openapiTemplateSrc) 219 if err != nil { 220 return "", errors.Wrapf(err, "parsing template for openapi_generated.go") 221 } 222 outputDir := filepath.Join(baseDir, outputPackage) 223 err = os.MkdirAll(outputDir, 0700) 224 if err != nil { 225 return "", errors.Wrapf(err, "creating directory %s", outputDir) 226 } 227 outFilePath := filepath.Join(outputDir, "openapi_generated.go") 228 outFile, err := os.Create(outFilePath) 229 if err != nil { 230 return "", errors.Wrapf(err, "creating file %s", outFilePath) 231 } 232 data := &openapiTemplateData{ 233 Path: path, 234 Dependents: dependents, 235 } 236 if semVer != "" { 237 data.Path = strings.ReplaceAll(path, "/pkg/", fmt.Sprintf("/%s/pkg/", semVer)) 238 data.Dependents = []string{} 239 for _, d := range dependents { 240 data.Dependents = append(data.Dependents, strings.ReplaceAll(d, "/pkg/", fmt.Sprintf("/%s/pkg/", semVer))) 241 } 242 } 243 err = tmpl.Execute(outFile, data) 244 defer func() { 245 err := outFile.Close() 246 if err != nil { 247 util.AppLogger().Errorf("error closing %s %v\n", outFilePath, err) 248 } 249 }() 250 if err != nil { 251 return "", errors.Wrapf(err, "templating %s", outFilePath) 252 } 253 return outputPackage, nil 254 } 255 256 // writeSchemaWriterToDisk code generates a simple main function that can be called to write the contents of all the 257 // OpenAPI structs out to JSON and YAML. It's implemented like this to allow us to automatically call the schema 258 // writer without requiring the user to write a command themselves. baseDir is the path to the module, 259 // outputPackage is the path to the outputPacakge for the code generator, 260 // and allImportPath is the path to the package where the generated map of all the structs is 261 func writeSchemaWriterToDisk(baseDir string, outputPackage string, allImportPath string, semVer string) (string, error) { 262 tmpl, err := template.New("schema_writer").Parse(schemaWriterTemplateSrc) 263 if err != nil { 264 return "", errors.Wrapf(err, "parsing template for %s", SchemaWriterSrcFileName) 265 } 266 outputDir := filepath.Join(baseDir, outputPackage) 267 err = os.MkdirAll(outputDir, 0700) 268 if err != nil { 269 return "", errors.Wrapf(err, "creating directory %s", outputDir) 270 } 271 outFilePath := filepath.Join(outputDir, SchemaWriterSrcFileName) 272 outFile, err := os.Create(outFilePath) 273 if err != nil { 274 return "", errors.Wrapf(err, "creating file %s", outFilePath) 275 } 276 data := &schemaWriterTemplateData{ 277 AllImportPath: allImportPath, 278 } 279 if semVer != "" { 280 data.AllImportPath = strings.ReplaceAll(allImportPath, "/pkg/", fmt.Sprintf("/%s/pkg/", semVer)) 281 } 282 err = tmpl.Execute(outFile, data) 283 defer func() { 284 err := outFile.Close() 285 if err != nil { 286 util.AppLogger().Errorf("error closing %s %v\n", outFilePath, err) 287 } 288 }() 289 if err != nil { 290 return "", errors.Wrapf(err, "templating %s", outFilePath) 291 } 292 return outputPackage, nil 293 } 294 295 // WriteSchemaToDisk is called by the code generated main function to marshal the contents of the OpenAPI structs and 296 // write them to disk. outputDir is the dir to write the json and yaml files to, 297 // you can also provide the title and version for the OpenAPI spec. 298 // definitions is the function that returns all the openapi definitions. 299 // WriteSchemaToDisk will rewrite the definitions to a dot-separated notation, reversing the initial domain name 300 func WriteSchemaToDisk(outputDir string, title string, version string, definitions common.GetOpenAPIDefinitions, 301 names []string) error { 302 err := os.MkdirAll(outputDir, 0700) 303 if err != nil { 304 return errors.Wrapf(err, "creating --output-directory %s", outputDir) 305 } 306 config := common.Config{ 307 Info: &spec.Info{ 308 InfoProps: spec.InfoProps{ 309 Version: version, 310 Title: title, 311 }, 312 }, 313 GetDefinitions: definitions, 314 GetDefinitionName: func(name string) (string, spec.Extensions) { 315 // For example "github.com/jenkins-x/jx/v2/pkg/apis/jenkins.io/v1.AppSpec" 316 parts := strings.Split(name, "/") 317 if len(parts) < 3 { 318 // Can't do anything with it, return raw 319 return name, nil 320 } 321 var result []string 322 for i, part := range parts { 323 // handle the domain at the start of the package 324 if i == 0 { 325 subparts := strings.Split(part, ".") 326 for j := len(subparts) - 1; j >= 0; j-- { 327 result = append(result, subparts[j]) 328 } 329 } else if i < len(parts)-1 { 330 // The docs generator can't handle a dot in the group name, so we remove it 331 result = append(result, strings.Replace(part, ".", "_", -1)) 332 } else { 333 result = append(result, part) 334 } 335 } 336 return strings.Join(result, "."), nil 337 }, 338 } 339 340 spec, err := builder.BuildOpenAPIDefinitionsForResources(&config, names...) 341 if err != nil { 342 return errors.Wrapf(err, "building openapi definitions for %s", names) 343 } 344 bytes, err := json.Marshal(spec) 345 if err != nil { 346 return errors.Wrapf(err, "marshaling openapi definitions to json for %s", names) 347 } 348 outFile := filepath.Join(outputDir, OpenApiV2JSON) 349 err = ioutil.WriteFile(outFile, bytes, 0600) 350 if err != nil { 351 return errors.Wrapf(err, "writing openapi definitions for %s to %s", names, outFile) 352 } 353 return nil 354 } 355 356 func packageToDirName(pkg string) string { 357 str := strings.Join(strings.Split(pkg, "/"), "_") 358 str = strings.Join(strings.Split(str, "."), "_") 359 return str 360 } 361 362 // GenerateSchema calls the generated schema writer and then loads the output and also writes out a yaml version. The 363 // outputDir is the base directory for writing the schemas to (they get put in the openapi-spec subdir), 364 // inputPackage is the package in which generated code lives, inputBase is the path to the module, 365 // title and version are used in the OpenAPI spec files. 366 func GenerateSchema(outputDir string, inputPackage string, inputBase string, title string, version string, gopath string) error { 367 schemaWriterSrc := filepath.Join(inputPackage, OpenApiDir, SchemaWriterSrcFileName) 368 schemaWriterBinary, err := ioutil.TempFile("", "") 369 outputDir = filepath.Join(outputDir, "openapi-spec") 370 defer func() { 371 err := util.DeleteFile(schemaWriterBinary.Name()) 372 if err != nil { 373 util.AppLogger().Warnf("error cleaning up tempfile %s created to compile %s to %v", 374 schemaWriterBinary.Name(), SchemaWriterSrcFileName, err) 375 } 376 }() 377 if err != nil { 378 return errors.Wrapf(err, "creating tempfile to compile %s to %v", SchemaWriterSrcFileName, err) 379 } 380 cmd := util.Command{ 381 Dir: inputBase, 382 Name: "go", 383 Args: []string{ 384 "build", 385 "-o", 386 schemaWriterBinary.Name(), 387 schemaWriterSrc, 388 }, 389 Env: map[string]string{ 390 "GO111MODULE": "on", 391 "GOPATH": gopath, 392 }, 393 } 394 out, err := cmd.RunWithoutRetry() 395 if err != nil { 396 return errors.Wrapf(err, "running %s, output %s", cmd.String(), out) 397 } 398 fileJSON := filepath.Join(outputDir, OpenApiV2JSON) 399 fileYAML := filepath.Join(outputDir, OpenApiV2YAML) 400 cmd = util.Command{ 401 Name: schemaWriterBinary.Name(), 402 Args: []string{ 403 "--output-directory", 404 outputDir, 405 "--title", 406 title, 407 "--version", 408 version, 409 }, 410 } 411 out, err = cmd.RunWithoutRetry() 412 if err != nil { 413 return errors.Wrapf(err, "running %s, output %s", cmd.String(), out) 414 } 415 // Convert to YAML as well 416 bytes, err := ioutil.ReadFile(fileJSON) 417 if err != nil { 418 return errors.Wrapf(err, "reading %s", fileJSON) 419 } 420 yamlBytes, err := yaml.JSONToYAML(bytes) 421 if err != nil { 422 return errors.Wrapf(err, "converting %s to yaml", fileJSON) 423 } 424 err = ioutil.WriteFile(fileYAML, yamlBytes, 0600) 425 if err != nil { 426 return errors.Wrapf(err, "writing %s", fileYAML) 427 } 428 return nil 429 } 430 431 func getOutputPackageForOpenApi(pkg string, groupWithVersion []string, outputPackage string) (string, error) { 432 if len(groupWithVersion) != 2 { 433 return "", errors.Errorf("groupWithVersion must be of length 2 but is %s", groupWithVersion) 434 } 435 version := groupWithVersion[1] 436 if version == "" { 437 version = "unversioned" 438 } 439 return filepath.Join(outputPackage, "openapi", fmt.Sprintf("%s_%s_%s", toValidPackageName(packageToDirName(pkg)), 440 toValidPackageName(groupWithVersion[0]), 441 toValidPackageName(version))), nil 442 } 443 444 func toValidPackageName(pkg string) string { 445 return strings.Replace(strings.Replace(pkg, "-", "_", -1), ".", "_", -1) 446 } 447 448 func generateOpenApiDependenciesStruct(outputPackage string, relativePackage string, outputBase string, 449 openApiDependencies []string, moduleDir string, moduleName string, boilerplateFile string, gopath string) ([]string, error) { 450 paths := make([]string, 0) 451 modulesRequirements, err := util.GetModuleRequirements(moduleDir, gopath) 452 if err != nil { 453 return nil, errors.Wrapf(err, "getting module requirements for %s", moduleDir) 454 } 455 for _, d := range openApiDependencies { 456 outputPackage, err := generate(d, outputPackage, relativePackage, outputBase, moduleName, boilerplateFile, gopath, modulesRequirements) 457 if err != nil { 458 return nil, errors.Wrapf(err, "generating open api dependency %s", d) 459 } 460 paths = append(paths, outputPackage) 461 } 462 return paths, nil 463 } 464 465 func generate(d string, outputPackage string, relativePackage string, outputBase string, moduleName string, boilerplateFile string, gopath string, modulesRequirements map[string]map[string]string) (string, error) { 466 // First, make sure we have the right files in our .jx GOPATH 467 // Use go mod to find out the dependencyVersion for our main tree 468 ds := strings.Split(d, ":") 469 if len(ds) != 4 { 470 return "", errors.Errorf("--open-api-dependency %s must be of the format path:package:group"+ 471 ":apiVersion", d) 472 } 473 path := ds[0] 474 pkg := ds[1] 475 group := ds[2] 476 version := ds[3] 477 groupsWithVersions := []string{ 478 fmt.Sprintf("%s:%s", group, version), 479 } 480 modules := false 481 if strings.Contains(path, "?modules") { 482 path = strings.TrimSuffix(path, "?modules") 483 modules = true 484 } 485 486 dependencyVersion := "master" 487 if moduleRequirement, ok := modulesRequirements[moduleName]; !ok { 488 util.AppLogger().Warnf("unable to find module requirement for %s, please add it to your go.mod, "+ 489 "for now using HEAD of the master branch", moduleName) 490 } else { 491 if requirementVersion, ok := moduleRequirement[path]; !ok { 492 util.AppLogger().Warnf("unable to find module requirement version for %s (module %s), "+ 493 "please add it to your go.mod, "+ 494 "for now using HEAD of the master branch", path, moduleName) 495 } else { 496 dependencyVersion = requirementVersion 497 } 498 } 499 500 if strings.HasPrefix(dependencyVersion, "v0.0.0-") { 501 parts := strings.Split(dependencyVersion, "-") 502 if len(parts) != 3 { 503 return "", errors.Errorf("unable to parse dependencyVersion %s", dependencyVersion) 504 } 505 // this is the sha 506 dependencyVersion = parts[2] 507 } 508 err := util.GoGet(path, dependencyVersion, gopath, modules, true, true) 509 if err != nil { 510 return "", errors.WithStack(err) 511 } 512 // Now we can run the generator against it 513 generator := openApiGenerator 514 modifiedOutputPackage, err := getOutputPackageForOpenApi(path, []string{group, version}, outputPackage) 515 if err != nil { 516 return "", errors.Wrapf(err, "getting filename for openapi structs for %s", d) 517 } 518 err = defaultGenerate(generator, 519 "openapi", 520 groupsWithVersions, 521 filepath.Join(path, pkg), 522 modifiedOutputPackage, 523 outputBase, 524 boilerplateFile, 525 gopath, 526 "--output-package", 527 modifiedOutputPackage) 528 if err != nil { 529 return "", errors.WithStack(err) 530 } 531 relativeOutputPackage, err := getOutputPackageForOpenApi(path, []string{group, version}, relativePackage) 532 if err != nil { 533 return "", errors.Wrapf(err, "getting filename for openapi structs for %s", d) 534 } 535 // the generator forgets to add the spec import in some cases 536 generatedFile := filepath.Join(relativeOutputPackage, "openapi_generated.go") 537 fs := token.NewFileSet() 538 f, err := parser.ParseFile(fs, generatedFile, nil, parser.ParseComments) 539 if err != nil { 540 return "", errors.Wrapf(err, "parsing %s", generatedFile) 541 } 542 found := false 543 for _, imp := range f.Imports { 544 if strings.Trim(imp.Path.Value, "\"") == "github.com/go-openapi/spec" { 545 found = true 546 break 547 } 548 } 549 if !found { 550 // Add the imports 551 for i := 0; i < len(f.Decls); i++ { 552 d := f.Decls[i] 553 554 switch d.(type) { 555 case *ast.FuncDecl: 556 // No action 557 case *ast.GenDecl: 558 dd := d.(*ast.GenDecl) 559 560 // IMPORT Declarations 561 if dd.Tok == token.IMPORT { 562 // Add the new import 563 iSpec := &ast.ImportSpec{Path: &ast.BasicLit{Value: strconv.Quote("github.com/go-openapi/spec")}} 564 dd.Specs = append(dd.Specs, iSpec) 565 } 566 } 567 } 568 569 // Sort the imports 570 ast.SortImports(fs, f) 571 var buf bytes.Buffer 572 err = format.Node(&buf, fs, f) 573 if err != nil { 574 return "", errors.Wrapf(err, "convert AST to []byte for %s", generatedFile) 575 } 576 // Manually add new lines after build tags 577 lines := strings.Split(string(buf.Bytes()), "\n") 578 buf.Reset() 579 for _, line := range lines { 580 buf.WriteString(line) 581 buf.WriteString("\r\n") 582 if strings.HasPrefix(line, "// +") { 583 buf.WriteString("\r\n") 584 } 585 } 586 587 err = ioutil.WriteFile(generatedFile, buf.Bytes(), 0600) 588 if err != nil { 589 return "", errors.Wrapf(err, "writing %s", generatedFile) 590 } 591 } 592 return modifiedOutputPackage, nil 593 }