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 }