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  }