sigs.k8s.io/kubebuilder/v3@v3.14.0/pkg/plugins/golang/v4/init.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package v4
    18  
    19  import (
    20  	"fmt"
    21  	"os"
    22  	"path/filepath"
    23  	"strings"
    24  	"unicode"
    25  
    26  	log "github.com/sirupsen/logrus"
    27  	"github.com/spf13/pflag"
    28  
    29  	"sigs.k8s.io/kubebuilder/v3/pkg/config"
    30  	"sigs.k8s.io/kubebuilder/v3/pkg/machinery"
    31  	"sigs.k8s.io/kubebuilder/v3/pkg/plugin"
    32  	"sigs.k8s.io/kubebuilder/v3/pkg/plugin/util"
    33  	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang"
    34  	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v4/scaffolds"
    35  )
    36  
    37  // Variables and function to check Go version requirements.
    38  var (
    39  	goVerMin = golang.MustParse("go1.19.0")
    40  	goVerMax = golang.MustParse("go2.0alpha1")
    41  )
    42  
    43  var _ plugin.InitSubcommand = &initSubcommand{}
    44  
    45  type initSubcommand struct {
    46  	config config.Config
    47  	// For help text.
    48  	commandName string
    49  
    50  	// boilerplate options
    51  	license string
    52  	owner   string
    53  
    54  	// go config options
    55  	repo string
    56  
    57  	// flags
    58  	fetchDeps          bool
    59  	skipGoVersionCheck bool
    60  }
    61  
    62  func (p *initSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
    63  	p.commandName = cliMeta.CommandName
    64  
    65  	subcmdMeta.Description = `Initialize a new project including the following files:
    66    - a "go.mod" with project dependencies
    67    - a "PROJECT" file that stores project configuration
    68    - a "Makefile" with several useful make targets for the project
    69    - several YAML files for project deployment under the "config" directory
    70    - a "cmd/main.go" file that creates the manager that will run the project controllers
    71  `
    72  	subcmdMeta.Examples = fmt.Sprintf(`  # Initialize a new project with your domain and name in copyright
    73    %[1]s init --plugins go/v4 --domain example.org --owner "Your name"
    74  
    75    # Initialize a new project defining a specific project version
    76    %[1]s init --plugins go/v4 --project-version 3
    77  `, cliMeta.CommandName)
    78  }
    79  
    80  func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) {
    81  	fs.BoolVar(&p.skipGoVersionCheck, "skip-go-version-check",
    82  		false, "if specified, skip checking the Go version")
    83  
    84  	// dependency args
    85  	fs.BoolVar(&p.fetchDeps, "fetch-deps", true, "ensure dependencies are downloaded")
    86  
    87  	// boilerplate args
    88  	fs.StringVar(&p.license, "license", "apache2",
    89  		"license to use to boilerplate, may be one of 'apache2', 'none'")
    90  	fs.StringVar(&p.owner, "owner", "", "owner to add to the copyright")
    91  
    92  	// project args
    93  	fs.StringVar(&p.repo, "repo", "", "name to use for go module (e.g., github.com/user/repo), "+
    94  		"defaults to the go package of the current working directory.")
    95  }
    96  
    97  func (p *initSubcommand) InjectConfig(c config.Config) error {
    98  	p.config = c
    99  
   100  	// Try to guess repository if flag is not set.
   101  	if p.repo == "" {
   102  		repoPath, err := golang.FindCurrentRepo()
   103  		if err != nil {
   104  			return fmt.Errorf("error finding current repository: %v", err)
   105  		}
   106  		p.repo = repoPath
   107  	}
   108  
   109  	return p.config.SetRepository(p.repo)
   110  }
   111  
   112  func (p *initSubcommand) PreScaffold(machinery.Filesystem) error {
   113  	// Ensure Go version is in the allowed range if check not turned off.
   114  	if !p.skipGoVersionCheck {
   115  		if err := golang.ValidateGoVersion(goVerMin, goVerMax); err != nil {
   116  			return err
   117  		}
   118  	}
   119  
   120  	// Check if the current directory has not files or directories which does not allow to init the project
   121  	return checkDir()
   122  }
   123  
   124  func (p *initSubcommand) Scaffold(fs machinery.Filesystem) error {
   125  	scaffolder := scaffolds.NewInitScaffolder(p.config, p.license, p.owner)
   126  	scaffolder.InjectFS(fs)
   127  	err := scaffolder.Scaffold()
   128  	if err != nil {
   129  		return err
   130  	}
   131  
   132  	if !p.fetchDeps {
   133  		log.Println("Skipping fetching dependencies.")
   134  		return nil
   135  	}
   136  
   137  	// Ensure that we are pinning controller-runtime version
   138  	// xref: https://github.com/kubernetes-sigs/kubebuilder/issues/997
   139  	err = util.RunCmd("Get controller runtime", "go", "get",
   140  		"sigs.k8s.io/controller-runtime@"+scaffolds.ControllerRuntimeVersion)
   141  	if err != nil {
   142  		return err
   143  	}
   144  
   145  	return nil
   146  }
   147  
   148  func (p *initSubcommand) PostScaffold() error {
   149  	err := util.RunCmd("Update dependencies", "go", "mod", "tidy")
   150  	if err != nil {
   151  		return err
   152  	}
   153  
   154  	fmt.Printf("Next: define a resource with:\n$ %s create api\n", p.commandName)
   155  	return nil
   156  }
   157  
   158  // checkDir will return error if the current directory has files which are not allowed.
   159  // Note that, it is expected that the directory to scaffold the project is cleaned.
   160  // Otherwise, it might face issues to do the scaffold.
   161  func checkDir() error {
   162  	err := filepath.Walk(".",
   163  		func(path string, info os.FileInfo, err error) error {
   164  			if err != nil {
   165  				return err
   166  			}
   167  			// Allow directory trees starting with '.'
   168  			if info.IsDir() && strings.HasPrefix(info.Name(), ".") && info.Name() != "." {
   169  				return filepath.SkipDir
   170  			}
   171  			// Allow files starting with '.'
   172  			if strings.HasPrefix(info.Name(), ".") {
   173  				return nil
   174  			}
   175  			// Allow files ending with '.md' extension
   176  			if strings.HasSuffix(info.Name(), ".md") && !info.IsDir() {
   177  				return nil
   178  			}
   179  			// Allow capitalized files except PROJECT
   180  			isCapitalized := true
   181  			for _, l := range info.Name() {
   182  				if !unicode.IsUpper(l) {
   183  					isCapitalized = false
   184  					break
   185  				}
   186  			}
   187  			if isCapitalized && info.Name() != "PROJECT" {
   188  				return nil
   189  			}
   190  			// Allow files in the following list
   191  			allowedFiles := []string{
   192  				"go.mod", // user might run `go mod init` instead of providing the `--flag` at init
   193  				"go.sum", // auto-generated file related to go.mod
   194  			}
   195  			for _, allowedFile := range allowedFiles {
   196  				if info.Name() == allowedFile {
   197  					return nil
   198  				}
   199  			}
   200  			// Do not allow any other file
   201  			return fmt.Errorf(
   202  				"target directory is not empty (only %s, files and directories with the prefix \".\", "+
   203  					"files with the suffix \".md\" or capitalized files name are allowed); "+
   204  					"found existing file %q", strings.Join(allowedFiles, ", "), path)
   205  		})
   206  	if err != nil {
   207  		return err
   208  	}
   209  	return nil
   210  }