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