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  }