github.skymusic.top/operator-framework/operator-sdk@v0.8.2/cmd/operator-sdk/new/cmd.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 new
    16  
    17  import (
    18  	"fmt"
    19  	"os"
    20  	"os/exec"
    21  	"path/filepath"
    22  	"strings"
    23  
    24  	"github.com/operator-framework/operator-sdk/internal/pkg/scaffold"
    25  	"github.com/operator-framework/operator-sdk/internal/pkg/scaffold/ansible"
    26  	"github.com/operator-framework/operator-sdk/internal/pkg/scaffold/helm"
    27  	"github.com/operator-framework/operator-sdk/internal/pkg/scaffold/input"
    28  	"github.com/operator-framework/operator-sdk/internal/util/projutil"
    29  
    30  	"github.com/pkg/errors"
    31  	log "github.com/sirupsen/logrus"
    32  	"github.com/spf13/cobra"
    33  	"sigs.k8s.io/controller-runtime/pkg/client/config"
    34  )
    35  
    36  func NewCmd() *cobra.Command {
    37  	newCmd := &cobra.Command{
    38  		Use:   "new <project-name>",
    39  		Short: "Creates a new operator application",
    40  		Long: `The operator-sdk new command creates a new operator application and
    41  generates a default directory layout based on the input <project-name>.
    42  
    43  <project-name> is the project name of the new operator. (e.g app-operator)
    44  
    45  For example:
    46  	$ mkdir $GOPATH/src/github.com/example.com/
    47  	$ cd $GOPATH/src/github.com/example.com/
    48  	$ operator-sdk new app-operator
    49  generates a skeletal app-operator application in $GOPATH/src/github.com/example.com/app-operator.
    50  `,
    51  		RunE: newFunc,
    52  	}
    53  
    54  	newCmd.Flags().StringVar(&apiVersion, "api-version", "", "Kubernetes apiVersion and has a format of $GROUP_NAME/$VERSION (e.g app.example.com/v1alpha1) - used with \"ansible\" or \"helm\" types")
    55  	newCmd.Flags().StringVar(&kind, "kind", "", "Kubernetes CustomResourceDefintion kind. (e.g AppService) - used with \"ansible\" or \"helm\" types")
    56  	newCmd.Flags().StringVar(&operatorType, "type", "go", "Type of operator to initialize (choices: \"go\", \"ansible\" or \"helm\")")
    57  	newCmd.Flags().StringVar(&depManager, "dep-manager", "modules", `Dependency manager the new project will use (choices: "dep", "modules")`)
    58  	newCmd.Flags().BoolVar(&skipGit, "skip-git-init", false, "Do not init the directory as a git repository")
    59  	newCmd.Flags().StringVar(&headerFile, "header-file", "", "Path to file containing headers for generated Go files. Copied to hack/boilerplate.go.txt")
    60  	newCmd.Flags().BoolVar(&generatePlaybook, "generate-playbook", false, "Generate a playbook skeleton. (Only used for --type ansible)")
    61  
    62  	newCmd.Flags().StringVar(&helmChartRef, "helm-chart", "", "Initialize helm operator with existing helm chart (<URL>, <repo>/<name>, or local path)")
    63  	newCmd.Flags().StringVar(&helmChartVersion, "helm-chart-version", "", "Specific version of the helm chart (default is latest version)")
    64  	newCmd.Flags().StringVar(&helmChartRepo, "helm-chart-repo", "", "Chart repository URL for the requested helm chart")
    65  
    66  	return newCmd
    67  }
    68  
    69  var (
    70  	apiVersion       string
    71  	kind             string
    72  	operatorType     string
    73  	projectName      string
    74  	depManager       string
    75  	headerFile       string
    76  	skipGit          bool
    77  	generatePlaybook bool
    78  
    79  	helmChartRef     string
    80  	helmChartVersion string
    81  	helmChartRepo    string
    82  )
    83  
    84  func newFunc(cmd *cobra.Command, args []string) error {
    85  	if err := parse(cmd, args); err != nil {
    86  		return err
    87  	}
    88  	mustBeNewProject()
    89  	if err := verifyFlags(); err != nil {
    90  		return err
    91  	}
    92  
    93  	log.Infof("Creating new %s operator '%s'.", strings.Title(operatorType), projectName)
    94  
    95  	switch operatorType {
    96  	case projutil.OperatorTypeGo:
    97  		if err := doGoScaffold(); err != nil {
    98  			return err
    99  		}
   100  		if err := getDeps(); err != nil {
   101  			return err
   102  		}
   103  	case projutil.OperatorTypeAnsible:
   104  		if err := doAnsibleScaffold(); err != nil {
   105  			return err
   106  		}
   107  	case projutil.OperatorTypeHelm:
   108  		if err := doHelmScaffold(); err != nil {
   109  			return err
   110  		}
   111  	}
   112  	if err := initGit(); err != nil {
   113  		return err
   114  	}
   115  
   116  	log.Info("Project creation complete.")
   117  	return nil
   118  }
   119  
   120  func parse(cmd *cobra.Command, args []string) error {
   121  	if len(args) != 1 {
   122  		return fmt.Errorf("command %s requires exactly one argument", cmd.CommandPath())
   123  	}
   124  	projectName = args[0]
   125  	if len(projectName) == 0 {
   126  		return fmt.Errorf("project name must not be empty")
   127  	}
   128  	return nil
   129  }
   130  
   131  // mustBeNewProject checks if the given project exists under the current diretory.
   132  // it exits with error when the project exists.
   133  func mustBeNewProject() {
   134  	fp := filepath.Join(projutil.MustGetwd(), projectName)
   135  	stat, err := os.Stat(fp)
   136  	if err != nil && os.IsNotExist(err) {
   137  		return
   138  	}
   139  	if err != nil {
   140  		log.Fatalf("Failed to determine if project (%v) exists", projectName)
   141  	}
   142  	if stat.IsDir() {
   143  		log.Fatalf("Project (%v) in (%v) path already exists. Please use a different project name or delete the existing one", projectName, fp)
   144  	}
   145  }
   146  
   147  func doGoScaffold() error {
   148  	cfg := &input.Config{
   149  		Repo:           filepath.Join(projutil.CheckAndGetProjectGoPkg(), projectName),
   150  		AbsProjectPath: filepath.Join(projutil.MustGetwd(), projectName),
   151  		ProjectName:    projectName,
   152  	}
   153  	s := &scaffold.Scaffold{}
   154  
   155  	if headerFile != "" {
   156  		err := s.Execute(cfg, &scaffold.Boilerplate{BoilerplateSrcPath: headerFile})
   157  		if err != nil {
   158  			return fmt.Errorf("boilerplate scaffold failed: (%v)", err)
   159  		}
   160  		s.BoilerplatePath = headerFile
   161  	}
   162  
   163  	var err error
   164  	switch m := projutil.DepManagerType(depManager); m {
   165  	case projutil.DepManagerDep:
   166  		err = s.Execute(cfg, &scaffold.GopkgToml{})
   167  	case projutil.DepManagerGoMod:
   168  		if goModOn, merr := projutil.GoModOn(); merr != nil {
   169  			return merr
   170  		} else if !goModOn {
   171  			log.Fatalf(`Dependency manager "%s" has been selected but go modules are not active. `+
   172  				`Activate modules then run "operator-sdk new %s".`, m, projectName)
   173  		}
   174  		err = s.Execute(cfg, &scaffold.GoMod{}, &scaffold.Tools{})
   175  	default:
   176  		err = projutil.ErrNoDepManager
   177  	}
   178  	if err != nil {
   179  		return fmt.Errorf("dependency manager file scaffold failed: (%v)", err)
   180  	}
   181  
   182  	err = s.Execute(cfg,
   183  		&scaffold.Cmd{},
   184  		&scaffold.Dockerfile{},
   185  		&scaffold.Entrypoint{},
   186  		&scaffold.UserSetup{},
   187  		&scaffold.ServiceAccount{},
   188  		&scaffold.Role{},
   189  		&scaffold.RoleBinding{},
   190  		&scaffold.Operator{},
   191  		&scaffold.Apis{},
   192  		&scaffold.Controller{},
   193  		&scaffold.Version{},
   194  		&scaffold.Gitignore{},
   195  	)
   196  	if err != nil {
   197  		return fmt.Errorf("new Go scaffold failed: (%v)", err)
   198  	}
   199  	return nil
   200  }
   201  
   202  func doAnsibleScaffold() error {
   203  	cfg := &input.Config{
   204  		AbsProjectPath: filepath.Join(projutil.MustGetwd(), projectName),
   205  		ProjectName:    projectName,
   206  	}
   207  
   208  	resource, err := scaffold.NewResource(apiVersion, kind)
   209  	if err != nil {
   210  		return fmt.Errorf("invalid apiVersion and kind: (%v)", err)
   211  	}
   212  
   213  	roleFiles := ansible.RolesFiles{Resource: *resource}
   214  	roleTemplates := ansible.RolesTemplates{Resource: *resource}
   215  
   216  	s := &scaffold.Scaffold{}
   217  	err = s.Execute(cfg,
   218  		&scaffold.ServiceAccount{},
   219  		&scaffold.Role{},
   220  		&scaffold.RoleBinding{},
   221  		&scaffold.CRD{Resource: resource},
   222  		&scaffold.CR{Resource: resource},
   223  		&ansible.BuildDockerfile{GeneratePlaybook: generatePlaybook},
   224  		&ansible.RolesReadme{Resource: *resource},
   225  		&ansible.RolesMetaMain{Resource: *resource},
   226  		&roleFiles,
   227  		&roleTemplates,
   228  		&ansible.RolesVarsMain{Resource: *resource},
   229  		&ansible.MoleculeTestLocalPlaybook{Resource: *resource},
   230  		&ansible.RolesDefaultsMain{Resource: *resource},
   231  		&ansible.RolesTasksMain{Resource: *resource},
   232  		&ansible.MoleculeDefaultMolecule{},
   233  		&ansible.BuildTestFrameworkDockerfile{},
   234  		&ansible.MoleculeTestClusterMolecule{},
   235  		&ansible.MoleculeDefaultPrepare{},
   236  		&ansible.MoleculeDefaultPlaybook{
   237  			GeneratePlaybook: generatePlaybook,
   238  			Resource:         *resource,
   239  		},
   240  		&ansible.BuildTestFrameworkAnsibleTestScript{},
   241  		&ansible.MoleculeDefaultAsserts{},
   242  		&ansible.MoleculeTestClusterPlaybook{Resource: *resource},
   243  		&ansible.RolesHandlersMain{Resource: *resource},
   244  		&ansible.Watches{
   245  			GeneratePlaybook: generatePlaybook,
   246  			Resource:         *resource,
   247  		},
   248  		&ansible.DeployOperator{},
   249  		&ansible.Travis{},
   250  		&ansible.MoleculeTestLocalMolecule{},
   251  		&ansible.MoleculeTestLocalPrepare{Resource: *resource},
   252  	)
   253  	if err != nil {
   254  		return fmt.Errorf("new ansible scaffold failed: (%v)", err)
   255  	}
   256  
   257  	// Remove placeholders from empty directories
   258  	err = os.Remove(filepath.Join(s.AbsProjectPath, roleFiles.Path))
   259  	if err != nil {
   260  		return fmt.Errorf("new ansible scaffold failed: (%v)", err)
   261  	}
   262  	err = os.Remove(filepath.Join(s.AbsProjectPath, roleTemplates.Path))
   263  	if err != nil {
   264  		return fmt.Errorf("new ansible scaffold failed: (%v)", err)
   265  	}
   266  
   267  	// Decide on playbook.
   268  	if generatePlaybook {
   269  		log.Infof("Generating %s playbook.", strings.Title(operatorType))
   270  
   271  		err := s.Execute(cfg,
   272  			&ansible.Playbook{Resource: *resource},
   273  		)
   274  		if err != nil {
   275  			return fmt.Errorf("new ansible playbook scaffold failed: (%v)", err)
   276  		}
   277  	}
   278  
   279  	// update deploy/role.yaml for the given resource r.
   280  	if err := scaffold.UpdateRoleForResource(resource, cfg.AbsProjectPath); err != nil {
   281  		return fmt.Errorf("failed to update the RBAC manifest for the resource (%v, %v): (%v)", resource.APIVersion, resource.Kind, err)
   282  	}
   283  	return nil
   284  }
   285  
   286  func doHelmScaffold() error {
   287  	cfg := &input.Config{
   288  		AbsProjectPath: filepath.Join(projutil.MustGetwd(), projectName),
   289  		ProjectName:    projectName,
   290  	}
   291  
   292  	createOpts := helm.CreateChartOptions{
   293  		ResourceAPIVersion: apiVersion,
   294  		ResourceKind:       kind,
   295  		Chart:              helmChartRef,
   296  		Version:            helmChartVersion,
   297  		Repo:               helmChartRepo,
   298  	}
   299  
   300  	resource, chart, err := helm.CreateChart(cfg.AbsProjectPath, createOpts)
   301  	if err != nil {
   302  		return fmt.Errorf("failed to create helm chart: %s", err)
   303  	}
   304  
   305  	valuesPath := filepath.Join("<project_dir>", helm.HelmChartsDir, chart.GetMetadata().GetName(), "values.yaml")
   306  	crSpec := fmt.Sprintf("# Default values copied from %s\n\n%s", valuesPath, chart.GetValues().GetRaw())
   307  
   308  	k8sCfg, err := config.GetConfig()
   309  	if err != nil {
   310  		return fmt.Errorf("failed to get kubernetes config: %s", err)
   311  	}
   312  	roleScaffold, err := helm.CreateRoleScaffold(k8sCfg, chart)
   313  	if err != nil {
   314  		return fmt.Errorf("failed to generate role scaffold: %s", err)
   315  	}
   316  
   317  	s := &scaffold.Scaffold{}
   318  	err = s.Execute(cfg,
   319  		&helm.Dockerfile{},
   320  		&helm.WatchesYAML{
   321  			Resource:  resource,
   322  			ChartName: chart.GetMetadata().GetName(),
   323  		},
   324  		&scaffold.ServiceAccount{},
   325  		roleScaffold,
   326  		&scaffold.RoleBinding{IsClusterScoped: roleScaffold.IsClusterScoped},
   327  		&helm.Operator{},
   328  		&scaffold.CRD{Resource: resource},
   329  		&scaffold.CR{
   330  			Resource: resource,
   331  			Spec:     crSpec,
   332  		},
   333  	)
   334  	if err != nil {
   335  		return fmt.Errorf("new helm scaffold failed: (%v)", err)
   336  	}
   337  
   338  	if err := scaffold.UpdateRoleForResource(resource, cfg.AbsProjectPath); err != nil {
   339  		return fmt.Errorf("failed to update the RBAC manifest for resource (%v, %v): (%v)", resource.APIVersion, resource.Kind, err)
   340  	}
   341  	return nil
   342  }
   343  
   344  func verifyFlags() error {
   345  	if operatorType != projutil.OperatorTypeGo && operatorType != projutil.OperatorTypeAnsible && operatorType != projutil.OperatorTypeHelm {
   346  		return errors.Wrap(projutil.ErrUnknownOperatorType{Type: operatorType}, "value of --type can only be `go`, `ansible`, or `helm`")
   347  	}
   348  	if operatorType != projutil.OperatorTypeAnsible && generatePlaybook {
   349  		return fmt.Errorf("value of --generate-playbook can only be used with --type `ansible`")
   350  	}
   351  
   352  	if len(helmChartRef) != 0 {
   353  		if operatorType != projutil.OperatorTypeHelm {
   354  			return fmt.Errorf("value of --helm-chart can only be used with --type=helm")
   355  		}
   356  	} else if len(helmChartRepo) != 0 {
   357  		return fmt.Errorf("value of --helm-chart-repo can only be used with --type=helm and --helm-chart")
   358  	} else if len(helmChartVersion) != 0 {
   359  		return fmt.Errorf("value of --helm-chart-version can only be used with --type=helm and --helm-chart")
   360  	}
   361  
   362  	if operatorType == projutil.OperatorTypeGo && (len(apiVersion) != 0 || len(kind) != 0) {
   363  		return fmt.Errorf("operators of type Go do not use --api-version or --kind")
   364  	}
   365  
   366  	// --api-version and --kind are required with --type=ansible and --type=helm, with one exception.
   367  	//
   368  	// If --type=helm and --helm-chart is set, --api-version and --kind are optional. If left unset,
   369  	// sane defaults are used when the specified helm chart is created.
   370  	if operatorType == projutil.OperatorTypeAnsible || operatorType == projutil.OperatorTypeHelm && len(helmChartRef) == 0 {
   371  		if len(apiVersion) == 0 {
   372  			return fmt.Errorf("value of --api-version must not have empty value")
   373  		}
   374  		if len(kind) == 0 {
   375  			return fmt.Errorf("value of --kind must not have empty value")
   376  		}
   377  		kindFirstLetter := string(kind[0])
   378  		if kindFirstLetter != strings.ToUpper(kindFirstLetter) {
   379  			return fmt.Errorf("value of --kind must start with an uppercase letter")
   380  		}
   381  		if strings.Count(apiVersion, "/") != 1 {
   382  			return fmt.Errorf("value of --api-version has wrong format (%v); format must be $GROUP_NAME/$VERSION (e.g app.example.com/v1alpha1)", apiVersion)
   383  		}
   384  	}
   385  	return nil
   386  }
   387  
   388  func execProjCmd(cmd string, args ...string) error {
   389  	dc := exec.Command(cmd, args...)
   390  	dc.Dir = filepath.Join(projutil.MustGetwd(), projectName)
   391  	return projutil.ExecCmd(dc)
   392  }
   393  
   394  func getDeps() error {
   395  	switch m := projutil.DepManagerType(depManager); m {
   396  	case projutil.DepManagerDep:
   397  		log.Info("Running dep ensure ...")
   398  		if err := execProjCmd("dep", "ensure", "-v"); err != nil {
   399  			return err
   400  		}
   401  	case projutil.DepManagerGoMod:
   402  		log.Info("Running go mod ...")
   403  		if err := execProjCmd("go", "mod", "vendor", "-v"); err != nil {
   404  			return err
   405  		}
   406  	default:
   407  		return projutil.ErrInvalidDepManager(depManager)
   408  	}
   409  	log.Info("Done getting dependencies")
   410  	return nil
   411  }
   412  
   413  func initGit() error {
   414  	if skipGit {
   415  		return nil
   416  	}
   417  	log.Info("Run git init ...")
   418  	if err := execProjCmd("git", "init"); err != nil {
   419  		return err
   420  	}
   421  	if err := execProjCmd("git", "add", "--all"); err != nil {
   422  		return err
   423  	}
   424  	if err := execProjCmd("git", "commit", "-q", "-m", "INITIAL COMMIT"); err != nil {
   425  		return err
   426  	}
   427  	log.Info("Run git init done")
   428  	return nil
   429  }