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 }