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