github.com/jmrodri/operator-sdk@v0.5.0/commands/operator-sdk/cmd/generate/openapi.go (about)

     1  // Copyright 2018 The Operator-SDK Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package generate
    16  
    17  import (
    18  	"fmt"
    19  	"io/ioutil"
    20  	"os"
    21  	"os/exec"
    22  	"path/filepath"
    23  	"strings"
    24  
    25  	genutil "github.com/operator-framework/operator-sdk/commands/operator-sdk/cmd/generate/internal"
    26  	"github.com/operator-framework/operator-sdk/internal/util/projutil"
    27  	"github.com/operator-framework/operator-sdk/pkg/scaffold"
    28  	"github.com/operator-framework/operator-sdk/pkg/scaffold/input"
    29  
    30  	"github.com/ghodss/yaml"
    31  	log "github.com/sirupsen/logrus"
    32  	"github.com/spf13/cobra"
    33  	apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
    34  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    35  )
    36  
    37  var headerFile string
    38  
    39  func NewGenerateOpenAPICmd() *cobra.Command {
    40  	openAPICmd := &cobra.Command{
    41  		Use:   "openapi",
    42  		Short: "Generates OpenAPI specs for API's",
    43  		Long: `generate openapi generates OpenAPI validation specs in Go from tagged types
    44  in all pkg/apis/<group>/<version> directories. Go code is generated under
    45  pkg/apis/<group>/<version>/zz_generated.openapi.go. CRD's are generated, or
    46  updated if they exist for a particular group + version + kind, under
    47  deploy/crds/<group>_<version>_<kind>_crd.yaml; OpenAPI V3 validation YAML
    48  is generated as a 'validation' object.
    49  
    50  Example:
    51  	$ operator-sdk generate openapi
    52  	$ tree pkg/apis
    53  	pkg/apis/
    54  	└── app
    55  		└── v1alpha1
    56  			├── zz_generated.openapi.go
    57  	$ tree deploy/crds
    58  	├── deploy/crds/app_v1alpha1_appservice_cr.yaml
    59  	├── deploy/crds/app_v1alpha1_appservice_crd.yaml
    60  `,
    61  		RunE: openAPIFunc,
    62  	}
    63  
    64  	openAPICmd.Flags().StringVar(&headerFile, "header-file", "", "Path to file containing headers for generated files.")
    65  
    66  	return openAPICmd
    67  }
    68  
    69  func openAPIFunc(cmd *cobra.Command, args []string) error {
    70  	if len(args) != 0 {
    71  		return fmt.Errorf("command %s doesn't accept any arguments", cmd.CommandPath())
    72  	}
    73  
    74  	return OpenAPIGen()
    75  }
    76  
    77  // OpenAPIGen generates OpenAPI validation specs for all CRD's in dirs.
    78  func OpenAPIGen() error {
    79  	projutil.MustInProjectRoot()
    80  
    81  	absProjectPath := projutil.MustGetwd()
    82  	repoPkg := projutil.CheckAndGetProjectGoPkg()
    83  	srcDir := filepath.Join(absProjectPath, "vendor", "k8s.io", "kube-openapi")
    84  	binDir := filepath.Join(absProjectPath, scaffold.BuildBinDir)
    85  
    86  	if err := buildOpenAPIGenBinary(binDir, srcDir); err != nil {
    87  		return err
    88  	}
    89  
    90  	gvMap, err := genutil.ParseGroupVersions()
    91  	if err != nil {
    92  		return fmt.Errorf("failed to parse group versions: (%v)", err)
    93  	}
    94  	gvb := &strings.Builder{}
    95  	for g, vs := range gvMap {
    96  		gvb.WriteString(fmt.Sprintf("%s:%v, ", g, vs))
    97  	}
    98  
    99  	log.Infof("Running OpenAPI code-generation for Custom Resource group versions: [%v]\n", gvb.String())
   100  
   101  	apisPkg := filepath.Join(repoPkg, scaffold.ApisDir)
   102  	fqApiStr := genutil.CreateFQApis(apisPkg, gvMap)
   103  	fqApis := strings.Split(fqApiStr, ",")
   104  	if err := openAPIGen(binDir, fqApis); err != nil {
   105  		return err
   106  	}
   107  
   108  	s := &scaffold.Scaffold{}
   109  	cfg := &input.Config{
   110  		Repo:           repoPkg,
   111  		AbsProjectPath: absProjectPath,
   112  		ProjectName:    filepath.Base(absProjectPath),
   113  	}
   114  	crdMap, err := getCRDGVKMap()
   115  	if err != nil {
   116  		return err
   117  	}
   118  	for g, vs := range gvMap {
   119  		for _, v := range vs {
   120  			gvks := crdMap[filepath.Join(g, v)]
   121  			for _, gvk := range gvks {
   122  				r, err := scaffold.NewResource(filepath.Join(gvk.Group, gvk.Version), gvk.Kind)
   123  				if err != nil {
   124  					return err
   125  				}
   126  				err = s.Execute(cfg,
   127  					&scaffold.CRD{Resource: r, IsOperatorGo: projutil.IsOperatorGo()},
   128  				)
   129  				if err != nil {
   130  					return err
   131  				}
   132  			}
   133  		}
   134  	}
   135  
   136  	log.Info("Code-generation complete.")
   137  	return nil
   138  }
   139  
   140  func buildOpenAPIGenBinary(binDir, codegenSrcDir string) error {
   141  	genDirs := []string{"./cmd/openapi-gen"}
   142  	return genutil.BuildCodegenBinaries(genDirs, binDir, codegenSrcDir)
   143  }
   144  
   145  func openAPIGen(binDir string, fqApis []string) (err error) {
   146  	if headerFile == "" {
   147  		f, err := ioutil.TempFile(scaffold.BuildBinDir, "")
   148  		if err != nil {
   149  			return err
   150  		}
   151  		headerFile = f.Name()
   152  		defer func() {
   153  			if err = os.RemoveAll(headerFile); err != nil {
   154  				log.Error(err)
   155  			}
   156  		}()
   157  	}
   158  	cgPath := filepath.Join(binDir, "openapi-gen")
   159  	for _, fqApi := range fqApis {
   160  		args := []string{
   161  			"--input-dirs", fqApi,
   162  			"--output-package", fqApi,
   163  			"--output-file-base", "zz_generated.openapi",
   164  			// openapi-gen requires a boilerplate file. Either use header or an
   165  			// empty file if header is empty.
   166  			"--go-header-file", headerFile,
   167  		}
   168  		cmd := exec.Command(cgPath, args...)
   169  		if projutil.IsGoVerbose() {
   170  			err = projutil.ExecCmd(cmd)
   171  		} else {
   172  			cmd.Stdout = ioutil.Discard
   173  			cmd.Stderr = ioutil.Discard
   174  			err = cmd.Run()
   175  		}
   176  		if err != nil {
   177  			return fmt.Errorf("failed to perform openapi code-generation: %v", err)
   178  		}
   179  	}
   180  	return nil
   181  }
   182  
   183  func getCRDGVKMap() (map[string][]metav1.GroupVersionKind, error) {
   184  	crdInfos, err := ioutil.ReadDir(scaffold.CRDsDir)
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  	crdMap := make(map[string][]metav1.GroupVersionKind)
   189  	for _, info := range crdInfos {
   190  		if filepath.Ext(info.Name()) == ".yaml" {
   191  			path := filepath.Join(scaffold.CRDsDir, info.Name())
   192  			b, err := ioutil.ReadFile(path)
   193  			if err != nil {
   194  				return nil, err
   195  			}
   196  			crd := &apiextv1beta1.CustomResourceDefinition{}
   197  			if err := yaml.Unmarshal(b, crd); err != nil {
   198  				return nil, err
   199  			}
   200  			if crd.Kind != "CustomResourceDefinition" {
   201  				continue
   202  			}
   203  			gv := filepath.Join(strings.Split(info.Name(), "_")[:2]...)
   204  			crdMap[gv] = append(crdMap[gv], metav1.GroupVersionKind{
   205  				Group:   crd.Spec.Group,
   206  				Version: crd.Spec.Version,
   207  				Kind:    crd.Spec.Names.Kind,
   208  			})
   209  		}
   210  	}
   211  	return crdMap, nil
   212  }