Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🏃 Homogenize commands through Scaffolder interface #1330

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
242 changes: 145 additions & 97 deletions cmd/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,157 +18,205 @@ package main

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

"github.com/spf13/cobra"
flag "github.com/spf13/pflag"

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

type apiOptions struct {
apiScaffolder scaffold.API
resourceFlag, controllerFlag *flag.Flag
type apiError struct {
err error
}

// runMake indicates whether to run make or not after scaffolding APIs
runMake bool
func (e apiError) Error() string {
return fmt.Sprintf("failed to create API: %v", e.err)
}

func newAPICmd() *cobra.Command {
options := &apiOptions{}

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) {
if err := run(options); err != nil {
log.Fatal(apiError{err})
}
},
}

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() {
internal.DieIfNotConfigured()

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

case "addon":
o.apiScaffolder.Plugins = append(o.apiScaffolder.Plugins, &addon.Plugin{})

default:
log.Fatalf("unknown pattern %q", o.pattern)
func (o *apiOptions) loadConfig() (*config.Config, error) {
projectConfig, err := config.Load()
if os.IsNotExist(err) {
return nil, errors.New("unable to find configuration file, project must be initialized")
}

if err := o.apiScaffolder.Validate(); err != nil {
log.Fatalln(err)
return projectConfig, err
}

func (o *apiOptions) validate(c *config.Config) error {
if err := o.resource.Validate(); err != nil {
return err
}

reader := bufio.NewReader(os.Stdin)
if !o.resourceFlag.Changed {
fmt.Println("Create Resource [y/n]")
o.apiScaffolder.DoResource = util.YesNo(reader)
o.doResource = internal.YesNo(reader)
}

if !o.controllerFlag.Changed {
fmt.Println("Create Controller [y/n]")
o.apiScaffolder.DoController = util.YesNo(reader)
}

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

if err := o.apiScaffolder.Scaffold(); err != nil {
log.Fatal(err)
o.doController = internal.YesNo(reader)
}

if err := o.postScaffold(); err != nil {
log.Fatal(err)
}
}
// 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 !c.IsV1() {
mengqiy marked this conversation as resolved.
Show resolved Hide resolved
// Check that resource doesn't exist or flag force was set
if !o.force {
resourceExists := false
for _, r := range c.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")
}
}
}

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)
// The following check is v2 specific as multi-group isn't enabled by default
if c.IsV2() {
// Check the group is the same for single-group projects
if !c.MultiGroup {
validGroup := true
for _, existingGroup := range c.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")
}
}
}
}

return nil
}

func newAPICommand() *cobra.Command {
options := apiOptions{
apiScaffolder: scaffold.API{},
}

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.

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
func (o *apiOptions) scaffolder(c *config.Config) (scaffold.Scaffolder, 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:
return nil, fmt.Errorf("unknown pattern %q", o.pattern)
}

options.bindCmdFlags(apiCmd)
return scaffold.NewAPIScaffolder(c, o.resource, o.doResource, o.doController, plugins), nil
}

return apiCmd
func (o *apiOptions) postScaffold(_ *config.Config) error {
return internal.RunCmd("Running make", "make")
}
20 changes: 1 addition & 19 deletions cmd/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,31 +17,13 @@ limitations under the License.
package main

import (
"fmt"

"github.com/spf13/cobra"

"sigs.k8s.io/kubebuilder/cmd/internal"
)

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(),
)

if !internal.ConfiguredAndV1() {
cmd.AddCommand(
newWebhookV2Cmd(),
)
}

return cmd
}
Loading