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

⚠️ Plugin chaining #1991

Closed
wants to merge 5 commits into from
Closed
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ test-unit: ## Run the unit tests
.PHONY: test-coverage
test-coverage: ## Run unit tests creating the output to report coverage
- rm -rf *.out # Remove all coverage files if exists
go test -race -failfast -tags=integration -coverprofile=coverage-all.out -coverpkg="./pkg/cli/...,./pkg/config/...,./pkg/internal/...,./pkg/model/...,./pkg/plugin/...,./pkg/plugins/golang,./pkg/plugins/internal/..." ./pkg/...
go test -race -failfast -tags=integration -coverprofile=coverage-all.out -coverpkg="./pkg/cli/...,./pkg/config/...,./pkg/internal/...,./pkg/machinery/...,./pkg/model/...,./pkg/plugin/...,./pkg/plugins/golang" ./pkg/...

.PHONY: test-integration
test-integration: ## Run the integration tests
Expand Down
2 changes: 2 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"sigs.k8s.io/kubebuilder/v3/pkg/cli"
cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2"
cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3"
declarativev1 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/declarative/v1"
pluginv2 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2"
pluginv3 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3"
)
Expand All @@ -34,6 +35,7 @@ func main() {
cli.WithPlugins(
&pluginv2.Plugin{},
&pluginv3.Plugin{},
&declarativev1.Plugin{},
),
cli.WithDefaultPlugins(cfgv2.Version, &pluginv2.Plugin{}),
cli.WithDefaultPlugins(cfgv3.Version, &pluginv3.Plugin{}),
Expand Down
84 changes: 31 additions & 53 deletions pkg/cli/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,80 +14,58 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

package cli // nolint:dupl
package cli //nolint:dupl

import (
"fmt"

"github.com/spf13/cobra"

"sigs.k8s.io/kubebuilder/v3/pkg/cli/internal/config"
"sigs.k8s.io/kubebuilder/v3/pkg/plugin"
)

const apiErrorMsg = "failed to create API"

func (c cli) newCreateAPICmd() *cobra.Command {
ctx := c.newAPIContext()
cmd := &cobra.Command{
Use: "api",
Short: "Scaffold a Kubernetes API",
Long: ctx.Description,
Example: ctx.Examples,
Use: "api",
Short: "Scaffold a Kubernetes API",
Long: `Scaffold a Kubernetes API.
Adirio marked this conversation as resolved.
Show resolved Hide resolved
`,
RunE: errCmdFunc(
fmt.Errorf("api subcommand requires an existing project"),
),
}

// Lookup the plugin for projectVersion and bind it to the command.
c.bindCreateAPI(ctx, cmd)
return cmd
}

func (c cli) newAPIContext() plugin.Context {
return plugin.Context{
CommandName: c.commandName,
Description: `Scaffold a Kubernetes API.
`,
}
}

// nolint:dupl
func (c cli) bindCreateAPI(ctx plugin.Context, cmd *cobra.Command) {
// In case no plugin was resolved, instead of failing the construction of the CLI, fail the execution of
// this subcommand. This allows the use of subcommands that do not require resolved plugins like help.
if len(c.resolvedPlugins) == 0 {
cmdErr(cmd, fmt.Errorf(noPluginError))
return
cmdErr(cmd, noResolvedPluginError{})
return cmd
}

var createAPIPlugin plugin.CreateAPI
for _, p := range c.resolvedPlugins {
tmpPlugin, isValid := p.(plugin.CreateAPI)
if isValid {
if createAPIPlugin != nil {
err := fmt.Errorf("duplicate API creation plugins (%s, %s), use a more specific plugin key",
plugin.KeyFor(createAPIPlugin), plugin.KeyFor(p))
cmdErr(cmd, err)
return
}
createAPIPlugin = tmpPlugin
}
}
// Obtain the plugin keys and subcommands from the plugins that implement plugin.CreateAPI.
pluginKeys, subcommands := c.filterSubcommands(
func(p plugin.Plugin) bool {
_, isValid := p.(plugin.CreateAPI)
return isValid
},
func(p plugin.Plugin) plugin.Subcommand {
return p.(plugin.CreateAPI).GetCreateAPISubcommand()
},
)

if createAPIPlugin == nil {
cmdErr(cmd, fmt.Errorf("resolved plugins do not provide an API creation plugin: %v", c.pluginKeys))
return
// Verify that there is at least one remaining plugin.
if len(*subcommands) == 0 {
cmdErr(cmd, noAvailablePluginError{"API creation"})
return cmd
}

cfg, err := config.LoadInitialized()
if err != nil {
cmdErr(cmd, err)
return
}
// Initialization methods.
options := c.initializationMethods(cmd, subcommands)

// Execution methods.
cmd.PreRunE, cmd.RunE, cmd.PostRunE = c.executionMethodsFuncs(pluginKeys, subcommands, options, apiErrorMsg)

subcommand := createAPIPlugin.GetCreateAPISubcommand()
subcommand.InjectConfig(cfg.Config)
subcommand.BindFlags(cmd.Flags())
subcommand.UpdateContext(&ctx)
cmd.Long = ctx.Description
cmd.Example = ctx.Examples
cmd.RunE = runECmdFunc(cfg, subcommand,
fmt.Sprintf("failed to create API with %q", plugin.KeyFor(createAPIPlugin)))
return cmd
}
28 changes: 20 additions & 8 deletions pkg/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@ limitations under the License.
package cli

import (
"errors"
"fmt"
"os"
"strings"

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

internalconfig "sigs.k8s.io/kubebuilder/v3/pkg/cli/internal/config"
"sigs.k8s.io/kubebuilder/v3/pkg/config"
yamlstore "sigs.k8s.io/kubebuilder/v3/pkg/config/store/yaml"
cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3"
"sigs.k8s.io/kubebuilder/v3/pkg/plugin"
)
Expand All @@ -36,8 +38,6 @@ const (

projectVersionFlag = "project-version"
pluginsFlag = "plugins"

noPluginError = "invalid config file please verify that the version and layout fields are set and valid"
)

// equalStringSlice checks if two string slices are equal.
Expand Down Expand Up @@ -96,6 +96,9 @@ type cli struct { //nolint:maligned

// Root command.
cmd *cobra.Command

// Underlying fs
fs afero.Fs
}

// New creates a new cli instance.
Expand Down Expand Up @@ -134,6 +137,7 @@ func newCLI(opts ...Option) (*cli, error) {
defaultProjectVersion: cfgv3.Version,
defaultPlugins: make(map[config.Version][]string),
plugins: make(map[string]plugin.Plugin),
fs: afero.NewOsFs(),
}

// Apply provided options.
Expand Down Expand Up @@ -191,18 +195,19 @@ func (c *cli) getInfoFromFlags() (string, []string, error) {
}

// getInfoFromConfigFile obtains the project version and plugin keys from the project config file.
func getInfoFromConfigFile() (config.Version, []string, error) {
func (c cli) getInfoFromConfigFile() (config.Version, []string, error) {
// Read the project configuration file
projectConfig, err := internalconfig.Read()
cfg := yamlstore.New(c.fs)
err := cfg.Load()
switch {
case err == nil:
case os.IsNotExist(err):
case errors.Is(err, os.ErrNotExist):
return config.Version{}, nil, nil
default:
return config.Version{}, nil, err
}

return getInfoFromConfig(projectConfig)
return getInfoFromConfig(cfg.Config())
}

// getInfoFromConfig obtains the project version and plugin keys from the project config.
Expand Down Expand Up @@ -297,7 +302,7 @@ func (c *cli) getInfo() error {
return err
}
// Get project version and plugin info from project configuration file
cfgProjectVersion, cfgPlugins, _ := getInfoFromConfigFile()
cfgProjectVersion, cfgPlugins, _ := c.getInfoFromConfigFile()
// We discard the error because not being able to read a project configuration file
// is not fatal for some commands. The ones that require it need to check its existence.

Expand Down Expand Up @@ -473,6 +478,13 @@ func (c cli) printDeprecationWarnings() {
}
}

// metadata returns CLI's metadata.
func (c cli) metadata() plugin.CLIMetadata {
return plugin.CLIMetadata{
CommandName: c.commandName,
}
}

// Run implements CLI.Run.
func (c cli) Run() error {
return c.cmd.Execute()
Expand Down
2 changes: 2 additions & 0 deletions pkg/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/spf13/afero"
"github.com/spf13/cobra"

"sigs.k8s.io/kubebuilder/v3/pkg/config"
Expand Down Expand Up @@ -585,6 +586,7 @@ var _ = Describe("CLI", func() {
defaultPlugins: map[config.Version][]string{
projectVersion: pluginKeys,
},
fs: afero.NewMemMapFs(),
}
c.cmd = c.newRootCmd()
Expect(c.getInfo()).To(Succeed())
Expand Down
Loading