Skip to content

Commit

Permalink
Homogenize commands through Scaffolder interface
Browse files Browse the repository at this point in the history
Signed-off-by: Adrian Orive <adrian.orive.oneca@gmail.com>
  • Loading branch information
Adirio committed Jan 23, 2020
1 parent d98c4cb commit 5e1cf20
Show file tree
Hide file tree
Showing 20 changed files with 1,552 additions and 1,253 deletions.
15 changes: 2 additions & 13 deletions cmd/alpha.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,10 @@ import (
"github.com/spf13/cobra"
)

// newAlphaCommand returns alpha subcommand which will be mounted
// at the root command by the caller.
func newAlphaCommand() *cobra.Command {
cmd := &cobra.Command{
func newAlphaCmd() *cobra.Command {
return &cobra.Command{
Use: "alpha",
Short: "Expose commands which are in experimental or early stages of development",
Long: `Command group for commands which are either experimental or in early stages of development`,
Example: `
# scaffolds webhook server
kubebuilder alpha webhook <params>
`,
}

cmd.AddCommand(
newWebhookCmd(),
)
return cmd
}
258 changes: 165 additions & 93 deletions cmd/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,158 +18,230 @@ package main

import (
"bufio"
"errors"
"fmt"
"log"
"os"
"os/exec"
"strings"

"github.com/spf13/cobra"
flag "github.com/spf13/pflag"
"sigs.k8s.io/kubebuilder/pkg/scaffold/project"

"sigs.k8s.io/kubebuilder/cmd/util"
"sigs.k8s.io/kubebuilder/pkg/scaffold"
"sigs.k8s.io/kubebuilder/pkg/scaffold/input"
"sigs.k8s.io/kubebuilder/pkg/scaffold/resource"
"sigs.k8s.io/kubebuilder/plugins/addon"
)

type apiOptions struct {
apiScaffolder scaffold.API
resourceFlag, controllerFlag *flag.Flag
func newAPICmd() *cobra.Command {
options := apiOptions{}

// runMake indicates whether to run make or not after scaffolding APIs
runMake bool
cmd := &cobra.Command{
Use: "api",
Short: "Scaffold a Kubernetes API",
Long: `Scaffold a Kubernetes API by creating a Resource definition and / or a Controller.
kubebuilder create api will prompt the user asking if it should scaffold the Resource and / or Controller. To only
scaffold a Controller for an existing Resource, select "n" for Resource. To only define
the schema for a Resource without writing a Controller, select "n" for Controller.
After the scaffold is written, api will run make on the project.
`,
Example: ` # Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate
kubebuilder create api --group ship --version v1beta1 --kind Frigate
# Edit the API Scheme
nano api/v1beta1/frigate_types.go
# Edit the Controller
nano controllers/frigate/frigate_controller.go
# Edit the Controller Test
nano controllers/frigate/frigate_controller_test.go
# Install CRDs into the Kubernetes cluster using kubectl apply
make install
# Regenerate code and run against the Kubernetes cluster configured by ~/.kube/config
make run
`,
Run: func(_ *cobra.Command, _ []string) {
options.run()
},
}

options.bindFlags(cmd)

return cmd
}

var _ commandOptions = &apiOptions{}

type apiOptions struct {
// pattern indicates that we should use a plugin to build according to a pattern
pattern string

resource *resource.Resource

// Check if we have to scaffold resource and/or controller
resourceFlag *flag.Flag
controllerFlag *flag.Flag
doResource bool
doController bool

// force indicates that the resource should be created even if it already exists
force bool

// runMake indicates whether to run make or not after scaffolding APIs
runMake bool
}

func (o *apiOptions) bindCmdFlags(cmd *cobra.Command) {
cmd.Flags().BoolVar(&o.runMake, "make", true,
"if true, run make after generating files")
cmd.Flags().BoolVar(&o.apiScaffolder.DoResource, "resource", true,
func (o *apiOptions) bindFlags(cmd *cobra.Command) {
cmd.Flags().BoolVar(&o.runMake, "make", true, "if true, run make after generating files")

cmd.Flags().BoolVar(&o.doResource, "resource", true,
"if set, generate the resource without prompting the user")
o.resourceFlag = cmd.Flag("resource")
cmd.Flags().BoolVar(&o.apiScaffolder.DoController, "controller", true,
cmd.Flags().BoolVar(&o.doController, "controller", true,
"if set, generate the controller without prompting the user")
o.controllerFlag = cmd.Flag("controller")

if os.Getenv("KUBEBUILDER_ENABLE_PLUGINS") != "" {
cmd.Flags().StringVar(&o.pattern, "pattern", "",
"generates an API following an extension pattern (addon)")
}
cmd.Flags().BoolVar(&o.apiScaffolder.Force, "force", false,

cmd.Flags().BoolVar(&o.force, "force", false,
"attempt to create resource even if it already exists")
o.apiScaffolder.Resource = resourceForFlags(cmd.Flags())
}

// resourceForFlags registers flags for Resource fields and returns the Resource
func resourceForFlags(f *flag.FlagSet) *resource.Resource {
r := &resource.Resource{}
f.StringVar(&r.Kind, "kind", "", "resource Kind")
f.StringVar(&r.Group, "group", "", "resource Group")
f.StringVar(&r.Version, "version", "", "resource Version")
f.BoolVar(&r.Namespaced, "namespaced", true, "resource is namespaced")
f.BoolVar(&r.CreateExampleReconcileBody, "example", true,
o.resource = &resource.Resource{}
cmd.Flags().StringVar(&o.resource.Kind, "kind", "", "resource Kind")
cmd.Flags().StringVar(&o.resource.Group, "group", "", "resource Group")
cmd.Flags().StringVar(&o.resource.Version, "version", "", "resource Version")
cmd.Flags().BoolVar(&o.resource.Namespaced, "namespaced", true, "resource is namespaced")
cmd.Flags().BoolVar(&o.resource.CreateExampleReconcileBody, "example", true,
"if true an example reconcile body should be written while scaffolding a resource.")
return r
}

// APICmd represents the resource command
func (o *apiOptions) runAddAPI() {
dieIfNoProject()

switch strings.ToLower(o.pattern) {
case "":
// Default pattern

case "addon":
o.apiScaffolder.Plugins = append(o.apiScaffolder.Plugins, &addon.Plugin{})
type apiError struct {
err error
}

default:
log.Fatalf("unknown pattern %q", o.pattern)
}
func (e apiError) Error() string {
return fmt.Sprintf("failed to create API: %v", e.err)
}

if err := o.apiScaffolder.Validate(); err != nil {
log.Fatalln(err)
func (o *apiOptions) run() {
// Step 1: load config
projectConfig, err := scaffold.LoadProjectFile("PROJECT")
if os.IsNotExist(err) {
log.Fatal(apiError{errors.New("unable to find configuration file, project must be initialized")})
} else if err != nil {
log.Fatal(apiError{err})
}

reader := bufio.NewReader(os.Stdin)
if !o.resourceFlag.Changed {
fmt.Println("Create Resource [y/n]")
o.apiScaffolder.DoResource = util.Yesno(reader)
// Step 2: validate
if err := o.validate(&projectConfig); err != nil {
log.Fatal(apiError{err})
}

if !o.controllerFlag.Changed {
fmt.Println("Create Controller [y/n]")
o.apiScaffolder.DoController = util.Yesno(reader)
// Step 3: create scaffolder
scaffolder, err := o.createScaffolder(&projectConfig)
if err != nil {
log.Fatal(apiError{err})
}

fmt.Println("Writing scaffold for you to edit...")

if err := o.apiScaffolder.Scaffold(); err != nil {
log.Fatal(err)
// Step 4: scaffold
if err := scaffolder.Scaffold(); err != nil {
log.Fatal(apiError{err})
}

// Step 5: finish
if err := o.postScaffold(); err != nil {
log.Fatal(err)
log.Fatal(apiError{err})
}
}

func (o *apiOptions) postScaffold() error {
if o.runMake {
fmt.Println("Running make...")
cm := exec.Command("make") // #nosec
cm.Stderr = os.Stderr
cm.Stdout = os.Stdout
if err := cm.Run(); err != nil {
return fmt.Errorf("error running make: %v", err)
}
func (o *apiOptions) validate(config *input.ProjectFile) error {
if err := o.resource.Validate(); err != nil {
return err
}
return nil
}

func newAPICommand() *cobra.Command {
options := apiOptions{
apiScaffolder: scaffold.API{},
reader := bufio.NewReader(os.Stdin)
if !o.resourceFlag.Changed {
fmt.Println("Create Resource [y/n]")
o.doResource = util.Yesno(reader)
}
if !o.controllerFlag.Changed {
fmt.Println("Create Controller [y/n]")
o.doController = util.Yesno(reader)
}

apiCmd := &cobra.Command{
Use: "api",
Short: "Scaffold a Kubernetes API",
Long: `Scaffold a Kubernetes API by creating a Resource definition and / or a Controller.
create resource will prompt the user for if it should scaffold the Resource and / or Controller. To only
scaffold a Controller for an existing Resource, select "n" for Resource. To only define
the schema for a Resource without writing a Controller, select "n" for Controller.
// In case we want to scaffold a resource API we need to do some checks
if o.doResource {
// Skip the following check for v1 as resources aren't tracked
if config.Version != project.Version1 {
// Check that resource doesn't exist or flag force was set
if !o.force {
resourceExists := false
for _, r := range config.Resources {
if r.Group == o.resource.Group &&
r.Version == o.resource.Version &&
r.Kind == o.resource.Kind {
resourceExists = true
break
}
}
if resourceExists {
return errors.New("API resource already exists")
}
}
}

After the scaffold is written, api will run make on the project.
`,
Example: ` # Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate
kubebuilder create api --group ship --version v1beta1 --kind Frigate
# Edit the API Scheme
nano api/v1beta1/frigate_types.go
// The following check is v2 specific as multi-group isn't enabled by default
if config.Version == project.Version2 {
// Check the group is the same for single-group projects
if !config.MultiGroup {
validGroup := true
for _, existingGroup := range config.ResourceGroups() {
if !strings.EqualFold(o.resource.Group, existingGroup) {
validGroup = false
break
}
}
if !validGroup {
return fmt.Errorf("multiple groups are not allowed by default, to enable multi-group visit %s",
"kubebuilder.io/migration/multi-group.html")
}
}
}
}

# Edit the Controller
nano controllers/frigate/frigate_controller.go
return nil
}

# Edit the Controller Test
nano controllers/frigate/frigate_controller_test.go
func (o *apiOptions) createScaffolder(config *input.ProjectFile) (scaffolder scaffold.Scaffolder, err error) {
plugins := make([]scaffold.Plugin, 0)
switch strings.ToLower(o.pattern) {
case "":
// Default pattern

# Install CRDs into the Kubernetes cluster using kubectl apply
make install
case "addon":
plugins = append(plugins, &addon.Plugin{})

# Regenerate code and run against the Kubernetes cluster configured by ~/.kube/config
make run
`,
Run: func(cmd *cobra.Command, args []string) {
options.runAddAPI()
},
default:
err = fmt.Errorf("unknown pattern %q", o.pattern)
return
}

options.bindCmdFlags(apiCmd)
scaffolder = scaffold.NewAPIScaffolder(config, o.resource, o.doResource, o.doController, plugins)
return
}

return apiCmd
func (o *apiOptions) postScaffold() error {
return util.RunCmd("Running make", "make")
}

// dieIfNoProject checks to make sure the command is run from a directory containing a project file.
Expand Down
22 changes: 1 addition & 21 deletions cmd/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,13 @@ limitations under the License.
package main

import (
"fmt"

"github.com/spf13/cobra"
)

func newCreateCmd() *cobra.Command {
cmd := &cobra.Command{
return &cobra.Command{
Use: "create",
Short: "Scaffold a Kubernetes API or webhook.",
Long: `Scaffold a Kubernetes API or webhook.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Coming soon.")
},
}
cmd.AddCommand(
newAPICommand(),
)

foundProject, version := getProjectVersion()
// It add webhook v2 command in the following 2 cases:
// - There are no PROJECT file found.
// - version == 2 is found in the PROJECT file.
if !foundProject || version == "2" {
cmd.AddCommand(
newWebhookV2Cmd(),
)
}

return cmd
}
Loading

0 comments on commit 5e1cf20

Please sign in to comment.